From 6465121a1e4f47b0a94ac65641bc32b88e0bfaa0 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Sat, 9 Dec 2017 01:15:34 -0800 Subject: [PATCH 001/185] Remove no-longer-relevant text from CHEATSHEET.rst Once my other PRs are merged, I think we'll have finished doing everything listed in the CHEATSHEET setup list, so we can delete it. --- CHEATSHEET.rst | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/CHEATSHEET.rst b/CHEATSHEET.rst index 93f6e33..49a8b8d 100644 --- a/CHEATSHEET.rst +++ b/CHEATSHEET.rst @@ -1,41 +1,3 @@ -Finishing setting up your project -================================= - -Thanks for using cookiecutter-trio! This is your project now; you can -customize it however you like. Here's some reminders of things you -might want to do to get started: - -* Check this into source control (``git init .; git add .; git - commit -m "Initial commit"``) - -* Add a CODE_OF_CONDUCT.md - -* Add a CONTRIBUTING.md - -* Search the source tree for COOKIECUTTER-TRIO-TODO to find other - places to fill in.* Enable `Read the Docs `__. (Note: this - project contains a ``.readthedocs.yml`` file that should be enough - to get things working.) - -* Set up continuous integration: Currently, this project is set up to - test on Linux and MacOS using Travis, on Windows using Appveyor, and - to test on PyPy. - - If that's what you want, then go to Travis and Appveyor and enable - testing for your repo. - - If that's not what you want, then you can trim the list by modifying - (or deleting) ``.travis.yml``, ``.appveyor.yml``, ``ci/travis.sh``. - -* Enable `Codecov `__ for your repo. - -* File bugs or pull requests on `cookiecutter-trio - `__ reporting any - problems or awkwardness you ran into (no matter how small!) - -* Delete this checklist once it's no longer useful - - Tips ==== From ff2fab3dfe8271981e0afc5add3417a2241d625b Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Fri, 15 Dec 2017 11:22:13 +0100 Subject: [PATCH 002/185] Remove unused imports --- pytest_trio/_tests/test_basic.py | 1 - pytest_trio/plugin.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/pytest_trio/_tests/test_basic.py b/pytest_trio/_tests/test_basic.py index 369da8a..6ae233f 100644 --- a/pytest_trio/_tests/test_basic.py +++ b/pytest_trio/_tests/test_basic.py @@ -1,5 +1,4 @@ import pytest -import trio def test_async_test_is_executed(testdir): diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 5546e29..5839992 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -1,6 +1,4 @@ """pytest-trio implementation.""" -import contextlib -import socket from traceback import format_exception from inspect import iscoroutinefunction, isgeneratorfunction try: From f470581c28ff7386ea560bdf8da6351c61dcd72f Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Fri, 15 Dec 2017 12:12:50 +0100 Subject: [PATCH 003/185] Fix coverage not covering module init --- .appveyor.yml | 2 +- ci/travis.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 27c1cc0..1ebc291 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -24,5 +24,5 @@ test_script: - "cd empty" # Make sure it's being imported from where we expect - "python -c \"import os, pytest_trio; print(os.path.dirname(pytest_trio.__file__))\"" - - "python -u -m pytest -W error -ra -v -s --pyargs pytest_trio --cov=pytest_trio --cov-config=../.coveragerc" + - "python -u -m coverage run --rcfile=../.coveragerc -m pytest -W error -ra -v -s --pyargs pytest_trio" - "codecov" diff --git a/ci/travis.sh b/ci/travis.sh index b296cc5..a59d32f 100755 --- a/ci/travis.sh +++ b/ci/travis.sh @@ -92,7 +92,7 @@ else mkdir empty cd empty - pytest -W error -ra -v --pyargs pytest_trio --cov=pytest_trio --cov-config=../.coveragerc --verbose + coverage run --rcfile=../.coveragerc -m pytest -W error -ra -v --pyargs pytest_trio --verbose bash <(curl -s https://codecov.io/bash) fi From 6bc7a85d83c3feedcf8585c364ba4b60c9485e29 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Fri, 15 Dec 2017 15:27:35 +0100 Subject: [PATCH 004/185] Bumpversion 0.1.1 -> 0.2.0 --- docs/source/history.rst | 17 +++++++++++++++++ newsfragments/15.removal.rst | 1 - newsfragments/17.feature.rst | 1 - pytest_trio/_version.py | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) delete mode 100644 newsfragments/15.removal.rst delete mode 100644 newsfragments/17.feature.rst diff --git a/docs/source/history.rst b/docs/source/history.rst index de61245..eddef52 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -5,6 +5,23 @@ Release history .. towncrier release notes start +Pytest_Trio 0.2.0 (2017-12-15) +------------------------------ + +Features +~~~~~~~~ + +- Heavy improvements, add async yield fixture, fix bugs, add tests etc. (`#17 + `__) + + +Deprecations and Removals +~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Remove unused_tcp_port{,_factory} fixtures (`#15 + `__) + + Pytest_Trio 0.1.1 (2017-12-08) ------------------------------ diff --git a/newsfragments/15.removal.rst b/newsfragments/15.removal.rst deleted file mode 100644 index d9781b8..0000000 --- a/newsfragments/15.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Remove unused_tcp_port{,_factory} fixtures diff --git a/newsfragments/17.feature.rst b/newsfragments/17.feature.rst deleted file mode 100644 index 0227be6..0000000 --- a/newsfragments/17.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Heavy improvements, add async yield fixture, fix bugs, add tests etc. diff --git a/pytest_trio/_version.py b/pytest_trio/_version.py index c0e6428..4f38c9e 100644 --- a/pytest_trio/_version.py +++ b/pytest_trio/_version.py @@ -1,3 +1,3 @@ # This file is imported from __init__.py and exec'd from setup.py -__version__ = "0.1.1" +__version__ = "0.2.0" From abe67d2b8e649a00501afd3ed0255d9bee8d9ea9 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Mon, 18 Dec 2017 22:12:06 +0100 Subject: [PATCH 005/185] Allow nursery creation in async yield fixtures by disabling fixture init in nursery (cause crash during teardown otherwise) --- .../_tests/test_async_yield_fixture.py | 37 +++++++++++++++++++ pytest_trio/plugin.py | 21 ++++------- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/pytest_trio/_tests/test_async_yield_fixture.py b/pytest_trio/_tests/test_async_yield_fixture.py index 88ed8ff..27b62e4 100644 --- a/pytest_trio/_tests/test_async_yield_fixture.py +++ b/pytest_trio/_tests/test_async_yield_fixture.py @@ -224,3 +224,40 @@ async def test_actual_test(fix1): # TODO: should trigger error instead of failure # result.assert_outcomes(error=1) result.assert_outcomes(failed=1) + + +@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6") +def test_async_yield_fixture_with_nursery(testdir): + + testdir.makepyfile( + """ + import pytest + import trio + + + async def handle_client(stream): + while True: + buff = await stream.receive_some(4) + await stream.send_all(buff) + + + @pytest.fixture + async def server(): + async with trio.open_nursery() as nursery: + listeners = await nursery.start(trio.serve_tcp, handle_client, 0) + yield listeners[0] + nursery.cancel_scope.cancel() + + + @pytest.mark.trio + async def test_actual_test(server): + stream = await trio.testing.open_stream_to_socket_listener(server) + await stream.send_all(b'ping') + rep = await stream.receive_some(4) + assert rep == b'ping' + """ + ) + + result = testdir.runpytest() + + result.assert_outcomes(passed=1) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 5839992..298f129 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -39,24 +39,17 @@ async def _bootstrap_fixture_and_run_test(**kwargs): async def _setup_async_fixtures_in(deps): resolved_deps = {**deps} - async def _resolve_and_update_deps(afunc, deps, entry): - deps[entry] = await afunc() - - async with trio.open_nursery() as nursery: - for depname, depval in resolved_deps.items(): - if isinstance(depval, BaseAsyncFixture): - nursery.start_soon( - _resolve_and_update_deps, depval.setup, resolved_deps, - depname - ) + for depname, depval in resolved_deps.items(): + if isinstance(depval, BaseAsyncFixture): + resolved_deps[depname] = await depval.setup() + return resolved_deps async def _teardown_async_fixtures_in(deps): - async with trio.open_nursery() as nursery: - for depval in deps.values(): - if isinstance(depval, BaseAsyncFixture): - nursery.start_soon(depval.teardown) + for depval in deps.values(): + if isinstance(depval, BaseAsyncFixture): + await depval.teardown() class BaseAsyncFixture: From 4df72f9d8fba5a0dbee8a3ef1cfaa2f1f37d7f16 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Wed, 20 Dec 2017 14:15:13 +0100 Subject: [PATCH 006/185] Fixtures setup/teardown takes place in recursive async context manager --- pytest_trio/_tests/test_sync_fixture.py | 43 ++++++++ pytest_trio/plugin.py | 132 +++++++++++++----------- 2 files changed, 115 insertions(+), 60 deletions(-) diff --git a/pytest_trio/_tests/test_sync_fixture.py b/pytest_trio/_tests/test_sync_fixture.py index a530051..961df23 100644 --- a/pytest_trio/_tests/test_sync_fixture.py +++ b/pytest_trio/_tests/test_sync_fixture.py @@ -44,3 +44,46 @@ def test_after(): result = testdir.runpytest() result.assert_outcomes(passed=3) + + +def test_single_yield_fixture_with_async_deps(testdir): + + testdir.makepyfile( + """ + import pytest + import trio + + events = [] + + @pytest.fixture + async def fix0(): + events.append('fix0 setup') + await trio.sleep(0) + return 'fix0' + + @pytest.fixture + def fix1(fix0): + events.append('fix1 setup') + yield 'fix1 - ' + fix0 + events.append('fix1 teardown') + + def test_before(): + assert not events + + @pytest.mark.trio + async def test_actual_test(fix1): + assert events == ['fix0 setup', 'fix1 setup'] + assert fix1 == 'fix1 - fix0' + + def test_after(): + assert events == [ + 'fix0 setup', + 'fix1 setup', + 'fix1 teardown', + ] + """ + ) + + result = testdir.runpytest() + + result.assert_outcomes(passed=3) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 298f129..1abc589 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -11,6 +11,7 @@ def isasyncgenfunction(x): import pytest import trio +from trio._util import acontextmanager from trio.testing import MockClock, trio_test @@ -29,27 +30,55 @@ def _trio_test_runner_factory(item): @trio_test async def _bootstrap_fixture_and_run_test(**kwargs): __tracebackhide__ = True - resolved_kwargs = await _setup_async_fixtures_in(kwargs) - await testfunc(**resolved_kwargs) - await _teardown_async_fixtures_in(kwargs) + user_exc = None + + try: + async with _setup_async_fixtures_in(kwargs) as resolved_kwargs: + try: + await testfunc(**resolved_kwargs) + except BaseException as exc: + # Regular pytest fixture don't have access to the test + # exception in there teardown, we mimic this behavior here. + user_exc = exc + except BaseException as exc: + # If we are here, the exception comes from the fixtures setup + # or teardown + if user_exc: + raise exc from user_exc + else: + raise exc + + # Finally re-raise or original exception coming from the test if needed + if user_exc: + raise user_exc return _bootstrap_fixture_and_run_test +@acontextmanager async def _setup_async_fixtures_in(deps): - resolved_deps = {**deps} + __tracebackhide__ = True - for depname, depval in resolved_deps.items(): - if isinstance(depval, BaseAsyncFixture): - resolved_deps[depname] = await depval.setup() + need_resolved_deps_stack = [(k, v) for k, v in deps.items() + if isinstance(v, BaseAsyncFixture)] - return resolved_deps + if not need_resolved_deps_stack: + yield deps + return + @acontextmanager + async def _recursive_setup(deps_stack): + __tracebackhide__ = True + name, dep = deps_stack.pop() + async with dep.setup() as resolved: + if not deps_stack: + yield [(name, resolved)] + else: + async with _recursive_setup(deps_stack) as remains_deps_stack_resolved: + yield remains_deps_stack_resolved + [(name, resolved)] -async def _teardown_async_fixtures_in(deps): - for depval in deps.values(): - if isinstance(depval, BaseAsyncFixture): - await depval.teardown() + async with _recursive_setup(need_resolved_deps_stack) as resolved_deps_stack: + yield {**deps, **dict(resolved_deps_stack)} class BaseAsyncFixture: @@ -61,49 +90,39 @@ def __init__(self, fixturedef, deps={}): self.fixturedef = fixturedef self.deps = deps self.setup_done = False - self.teardown_done = False self.result = None - self.lock = trio.Lock() + @acontextmanager async def setup(self): - async with self.lock: - if not self.setup_done: - self.result = await self._setup() - self.setup_done = True - return self.result + __tracebackhide__ = True + if self.setup_done: + yield self.result + else: + async with _setup_async_fixtures_in(self.deps) as resolved_deps: + async with self._setup(resolved_deps) as self.result: + self.setup_done = True + yield self.result async def _setup(self): raise NotImplementedError() - async def teardown(self): - async with self.lock: - if not self.teardown_done: - await self._teardown() - self.teardown_done = True - - async def _teardown(self): - raise NotImplementedError() - class AsyncYieldFixture(BaseAsyncFixture): """ Async generator fixture. """ - def __init__(self, *args): - super().__init__(*args) - self.agen = None + @acontextmanager + async def _setup(self, resolved_deps): + __tracebackhide__ = True + agen = self.fixturedef.func(**resolved_deps) - async def _setup(self): - resolved_deps = await _setup_async_fixtures_in(self.deps) - self.agen = self.fixturedef.func(**resolved_deps) - return await self.agen.asend(None) + yield await agen.asend(None) - async def _teardown(self): try: - await self.agen.asend(None) + await agen.asend(None) except StopAsyncIteration: - await _teardown_async_fixtures_in(self.deps) + pass else: raise RuntimeError('Only one yield in fixture is allowed') @@ -113,12 +132,10 @@ class SyncFixtureWithAsyncDeps(BaseAsyncFixture): Synchronous function fixture with asynchronous dependencies fixtures. """ - async def _setup(self): - resolved_deps = await _setup_async_fixtures_in(self.deps) - return self.fixturedef.func(**resolved_deps) - - async def _teardown(self): - await _teardown_async_fixtures_in(self.deps) + @acontextmanager + async def _setup(self, resolved_deps): + __tracebackhide__ = True + yield self.fixturedef.func(**resolved_deps) class SyncYieldFixtureWithAsyncDeps(BaseAsyncFixture): @@ -126,20 +143,17 @@ class SyncYieldFixtureWithAsyncDeps(BaseAsyncFixture): Synchronous generator fixture with asynchronous dependencies fixtures. """ - def __init__(self, *args): - super().__init__(*args) - self.agen = None + @acontextmanager + async def _setup(self, resolved_deps): + __tracebackhide__ = True + gen = self.fixturedef.func(**resolved_deps) - async def _setup(self): - resolved_deps = await _setup_async_fixtures_in(self.deps) - self.gen = self.fixturedef.func(**resolved_deps) - return self.gen.send(None) + yield gen.send(None) - async def _teardown(self): try: - await self.gen.send(None) + gen.send(None) except StopIteration: - await _teardown_async_fixtures_in(self.deps) + pass else: raise RuntimeError('Only one yield in fixture is allowed') @@ -149,12 +163,10 @@ class AsyncFixture(BaseAsyncFixture): Regular async fixture (i.e. coroutine). """ - async def _setup(self): - resolved_deps = await _setup_async_fixtures_in(self.deps) - return await self.fixturedef.func(**resolved_deps) - - async def _teardown(self): - await _teardown_async_fixtures_in(self.deps) + @acontextmanager + async def _setup(self, resolved_deps): + __tracebackhide__ = True + yield await self.fixturedef.func(**resolved_deps) def _install_async_fixture_if_needed(fixturedef, request): From 48aa00f842a65e4ee343c08efcba0972bfef70d9 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Sat, 23 Dec 2017 14:54:58 +0100 Subject: [PATCH 007/185] Add nursery fixture --- pytest_trio/_tests/test_nursery_fixture.py | 22 ++++++++++++ pytest_trio/plugin.py | 42 +++++++++++++--------- 2 files changed, 48 insertions(+), 16 deletions(-) create mode 100644 pytest_trio/_tests/test_nursery_fixture.py diff --git a/pytest_trio/_tests/test_nursery_fixture.py b/pytest_trio/_tests/test_nursery_fixture.py new file mode 100644 index 0000000..7719fa1 --- /dev/null +++ b/pytest_trio/_tests/test_nursery_fixture.py @@ -0,0 +1,22 @@ +import pytest +import trio + + +async def handle_client(stream): + while True: + buff = await stream.receive_some(4) + await stream.send_all(buff) + + +@pytest.fixture +async def server(nursery): + listeners = await nursery.start(trio.serve_tcp, handle_client, 0) + return listeners[0] + + +@pytest.mark.trio +async def test_try(server): + stream = await trio.testing.open_stream_to_socket_listener(server) + await stream.send_all(b'ping') + rep = await stream.receive_some(4) + assert rep == b'ping' diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 1abc589..b304cf4 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -31,22 +31,27 @@ def _trio_test_runner_factory(item): async def _bootstrap_fixture_and_run_test(**kwargs): __tracebackhide__ = True user_exc = None - - try: - async with _setup_async_fixtures_in(kwargs) as resolved_kwargs: - try: - await testfunc(**resolved_kwargs) - except BaseException as exc: - # Regular pytest fixture don't have access to the test - # exception in there teardown, we mimic this behavior here. - user_exc = exc - except BaseException as exc: - # If we are here, the exception comes from the fixtures setup - # or teardown - if user_exc: - raise exc from user_exc - else: - raise exc + # Open the nursery exposed as fixture + async with trio.open_nursery() as nursery: + item._trio_nursery = nursery + try: + async with _setup_async_fixtures_in(kwargs) as resolved_kwargs: + try: + await testfunc(**resolved_kwargs) + except BaseException as exc: + # Regular pytest fixture don't have access to the test + # exception in there teardown, we mimic this behavior here. + user_exc = exc + except BaseException as exc: + # If we are here, the exception comes from the fixtures setup + # or teardown + if user_exc: + raise exc from user_exc + else: + raise exc + finally: + # No matter what the nursery fixture should be closed when test is over + nursery.cancel_scope.cancel() # Finally re-raise or original exception coming from the test if needed if user_exc: @@ -220,3 +225,8 @@ def mock_clock(): @pytest.fixture def autojump_clock(): return MockClock(autojump_threshold=0) + + +@pytest.fixture +async def nursery(request): + return request.node._trio_nursery From b199c2a4773bd141e3fcf8f4ee318c9ccb957ab9 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Sat, 23 Dec 2017 15:00:47 +0100 Subject: [PATCH 008/185] Fix async generator for python 3.5 --- pytest_trio/plugin.py | 28 ++++++++++++++++++---------- setup.py | 3 ++- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index b304cf4..136bed8 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -13,6 +13,7 @@ def isasyncgenfunction(x): import trio from trio._util import acontextmanager from trio.testing import MockClock, trio_test +from async_generator import async_generator, yield_ def pytest_configure(config): @@ -61,6 +62,7 @@ async def _bootstrap_fixture_and_run_test(**kwargs): @acontextmanager +@async_generator async def _setup_async_fixtures_in(deps): __tracebackhide__ = True @@ -68,22 +70,23 @@ async def _setup_async_fixtures_in(deps): if isinstance(v, BaseAsyncFixture)] if not need_resolved_deps_stack: - yield deps + await yield_(deps) return @acontextmanager + @async_generator async def _recursive_setup(deps_stack): __tracebackhide__ = True name, dep = deps_stack.pop() async with dep.setup() as resolved: if not deps_stack: - yield [(name, resolved)] + await yield_([(name, resolved)]) else: async with _recursive_setup(deps_stack) as remains_deps_stack_resolved: - yield remains_deps_stack_resolved + [(name, resolved)] + await yield_(remains_deps_stack_resolved + [(name, resolved)]) async with _recursive_setup(need_resolved_deps_stack) as resolved_deps_stack: - yield {**deps, **dict(resolved_deps_stack)} + await yield_({**deps, **dict(resolved_deps_stack)}) class BaseAsyncFixture: @@ -98,15 +101,16 @@ def __init__(self, fixturedef, deps={}): self.result = None @acontextmanager + @async_generator async def setup(self): __tracebackhide__ = True if self.setup_done: - yield self.result + await yield_(self.result) else: async with _setup_async_fixtures_in(self.deps) as resolved_deps: async with self._setup(resolved_deps) as self.result: self.setup_done = True - yield self.result + await yield_(self.result) async def _setup(self): raise NotImplementedError() @@ -118,11 +122,12 @@ class AsyncYieldFixture(BaseAsyncFixture): """ @acontextmanager + @async_generator async def _setup(self, resolved_deps): __tracebackhide__ = True agen = self.fixturedef.func(**resolved_deps) - yield await agen.asend(None) + await yield_(await agen.asend(None)) try: await agen.asend(None) @@ -138,9 +143,10 @@ class SyncFixtureWithAsyncDeps(BaseAsyncFixture): """ @acontextmanager + @async_generator async def _setup(self, resolved_deps): __tracebackhide__ = True - yield self.fixturedef.func(**resolved_deps) + await yield_(self.fixturedef.func(**resolved_deps)) class SyncYieldFixtureWithAsyncDeps(BaseAsyncFixture): @@ -149,11 +155,12 @@ class SyncYieldFixtureWithAsyncDeps(BaseAsyncFixture): """ @acontextmanager + @async_generator async def _setup(self, resolved_deps): __tracebackhide__ = True gen = self.fixturedef.func(**resolved_deps) - yield gen.send(None) + await yield_(gen.send(None)) try: gen.send(None) @@ -169,9 +176,10 @@ class AsyncFixture(BaseAsyncFixture): """ @acontextmanager + @async_generator async def _setup(self, resolved_deps): __tracebackhide__ = True - yield await self.fixturedef.func(**resolved_deps) + await yield_(await self.fixturedef.func(**resolved_deps)) def _install_async_fixture_if_needed(fixturedef, request): diff --git a/setup.py b/setup.py index 3f49b99..280e767 100644 --- a/setup.py +++ b/setup.py @@ -14,9 +14,10 @@ author_email="emmanuel.leblond@gmail.com", license="MIT -or- Apache License 2.0", packages=find_packages(), - entry_points={'pytest11': ['trio = pytest_trio.plugin',]}, + entry_points={'pytest11': ['trio = pytest_trio.plugin']}, install_requires=[ "trio", + "async_generator >= 1.6", ], keywords=[ 'async', From 57d564c9ac91ca968d88131a1aa93fd9edc92d99 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Tue, 26 Dec 2017 12:28:27 +0100 Subject: [PATCH 009/185] Fix style --- pytest_trio/plugin.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 136bed8..813b1d1 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -66,8 +66,9 @@ async def _bootstrap_fixture_and_run_test(**kwargs): async def _setup_async_fixtures_in(deps): __tracebackhide__ = True - need_resolved_deps_stack = [(k, v) for k, v in deps.items() - if isinstance(v, BaseAsyncFixture)] + need_resolved_deps_stack = [ + (k, v) for k, v in deps.items() if isinstance(v, BaseAsyncFixture) + ] if not need_resolved_deps_stack: await yield_(deps) @@ -82,10 +83,16 @@ async def _recursive_setup(deps_stack): if not deps_stack: await yield_([(name, resolved)]) else: - async with _recursive_setup(deps_stack) as remains_deps_stack_resolved: - await yield_(remains_deps_stack_resolved + [(name, resolved)]) - - async with _recursive_setup(need_resolved_deps_stack) as resolved_deps_stack: + async with _recursive_setup( + deps_stack + ) as remains_deps_stack_resolved: + await yield_( + remains_deps_stack_resolved + [(name, resolved)] + ) + + async with _recursive_setup( + need_resolved_deps_stack + ) as resolved_deps_stack: await yield_({**deps, **dict(resolved_deps_stack)}) From c35567df7d14c1cc3c2383669acda550105757a2 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Wed, 3 Jan 2018 19:01:17 +0100 Subject: [PATCH 010/185] Bumpversion 0.2.0 -> 0.3.0 --- docs/source/history.rst | 9 ++++++++- pytest_trio/_version.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/source/history.rst b/docs/source/history.rst index eddef52..d1ff8a3 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -5,12 +5,19 @@ Release history .. towncrier release notes start -Pytest_Trio 0.2.0 (2017-12-15) +Pytest_Trio 0.3.0 (2018-01-03) ------------------------------ Features ~~~~~~~~ +- Add ``nursery`` fixture and improve teardown handling for yield fixture (`#25 + `__) + + +Pytest_Trio 0.2.0 (2017-12-15) +------------------------------ + - Heavy improvements, add async yield fixture, fix bugs, add tests etc. (`#17 `__) diff --git a/pytest_trio/_version.py b/pytest_trio/_version.py index 4f38c9e..f879b44 100644 --- a/pytest_trio/_version.py +++ b/pytest_trio/_version.py @@ -1,3 +1,3 @@ # This file is imported from __init__.py and exec'd from setup.py -__version__ = "0.2.0" +__version__ = "0.3.0" From 1a2a88f8fdaf3a884178a29853140b62b604c9ed Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Mon, 12 Feb 2018 23:59:57 -0800 Subject: [PATCH 011/185] Add 'Framework :: Trio' classifier to setup.py --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 280e767..0fb7934 100644 --- a/setup.py +++ b/setup.py @@ -38,5 +38,6 @@ "Topic :: System :: Networking", "Topic :: Software Development :: Testing", "Framework :: Pytest", + "Framework :: Trio", ], ) From 880f84c61b45ddbf59428fa7eac0fa168b55a85a Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Thu, 22 Mar 2018 19:18:06 +0100 Subject: [PATCH 012/185] Add test_async_yield_fixture_crashed_teardown_allow_other_teardowns --- .../_tests/test_async_yield_fixture.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/pytest_trio/_tests/test_async_yield_fixture.py b/pytest_trio/_tests/test_async_yield_fixture.py index 27b62e4..8f6aca1 100644 --- a/pytest_trio/_tests/test_async_yield_fixture.py +++ b/pytest_trio/_tests/test_async_yield_fixture.py @@ -261,3 +261,51 @@ async def test_actual_test(server): result = testdir.runpytest() result.assert_outcomes(passed=1) + + +@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6") +def test_async_yield_fixture_crashed_teardown_allow_other_teardowns(testdir): + + testdir.makepyfile( + """ + import pytest + import trio + + events = [] + + @pytest.fixture + async def good_fixture(): + async with trio.open_nursery() as nursery: + events.append('good_fixture setup') + yield + events.append('good_fixture teardown') + + @pytest.fixture + async def bad_fixture(): + async with trio.open_nursery() as nursery: + events.append('bad_fixture setup') + yield + events.append('bad_fixture teardown') + raise RuntimeError('Crash during fixture teardown') + # Cannot cancel offtask's scope + + def test_before(): + assert not events + + @pytest.mark.trio + async def test_actual_test(bad_fixture, good_fixture): + pass + + def test_after(): + assert events == [ + 'good_fixture setup', + 'bad_fixture setup', + 'bad_fixture teardown', + ] + """ + ) + + result = testdir.runpytest() + + result.assert_outcomes(failed=1, passed=2) + result.stdout.re_match_lines('E RuntimeError: Crash during fixture teardown') From 4bd2ec257e93506e177304d0c334608ca3b00734 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Sat, 24 Mar 2018 09:55:20 +0100 Subject: [PATCH 013/185] Fix crash when exception occurs in yield fixture's teardown --- .../_tests/test_async_yield_fixture.py | 4 +- pytest_trio/_tests/test_sync_fixture.py | 49 +++++++++++++++++++ pytest_trio/plugin.py | 30 ++++++------ 3 files changed, 67 insertions(+), 16 deletions(-) diff --git a/pytest_trio/_tests/test_async_yield_fixture.py b/pytest_trio/_tests/test_async_yield_fixture.py index 8f6aca1..f7c7b08 100644 --- a/pytest_trio/_tests/test_async_yield_fixture.py +++ b/pytest_trio/_tests/test_async_yield_fixture.py @@ -287,7 +287,6 @@ async def bad_fixture(): yield events.append('bad_fixture teardown') raise RuntimeError('Crash during fixture teardown') - # Cannot cancel offtask's scope def test_before(): assert not events @@ -301,6 +300,7 @@ def test_after(): 'good_fixture setup', 'bad_fixture setup', 'bad_fixture teardown', + 'good_fixture teardown', ] """ ) @@ -308,4 +308,4 @@ def test_after(): result = testdir.runpytest() result.assert_outcomes(failed=1, passed=2) - result.stdout.re_match_lines('E RuntimeError: Crash during fixture teardown') + result.stdout.re_match_lines([r'E\W+RuntimeError: Crash during fixture teardown']) diff --git a/pytest_trio/_tests/test_sync_fixture.py b/pytest_trio/_tests/test_sync_fixture.py index 961df23..a525f98 100644 --- a/pytest_trio/_tests/test_sync_fixture.py +++ b/pytest_trio/_tests/test_sync_fixture.py @@ -87,3 +87,52 @@ def test_after(): result = testdir.runpytest() result.assert_outcomes(passed=3) + + +def test_sync_yield_fixture_crashed_teardown_allow_other_teardowns(testdir): + + testdir.makepyfile( + """ + import pytest + import trio + + events = [] + + @pytest.fixture + async def force_async_fixture(): + pass + + @pytest.fixture + def good_fixture(force_async_fixture): + events.append('good_fixture setup') + yield + events.append('good_fixture teardown') + + @pytest.fixture + def bad_fixture(force_async_fixture): + events.append('bad_fixture setup') + yield + events.append('bad_fixture teardown') + raise RuntimeError('Crash during fixture teardown') + + def test_before(): + assert not events + + @pytest.mark.trio + async def test_actual_test(bad_fixture, good_fixture): + pass + + def test_after(): + assert events == [ + 'good_fixture setup', + 'bad_fixture setup', + 'bad_fixture teardown', + 'good_fixture teardown', + ] + """ + ) + + result = testdir.runpytest() + + result.assert_outcomes(failed=1, passed=2) + result.stdout.re_match_lines([r'E\W+RuntimeError: Crash during fixture teardown']) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 813b1d1..b8b36b5 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -134,14 +134,15 @@ async def _setup(self, resolved_deps): __tracebackhide__ = True agen = self.fixturedef.func(**resolved_deps) - await yield_(await agen.asend(None)) - try: - await agen.asend(None) - except StopAsyncIteration: - pass - else: - raise RuntimeError('Only one yield in fixture is allowed') + await yield_(await agen.asend(None)) + finally: + try: + await agen.asend(None) + except StopAsyncIteration: + pass + else: + raise RuntimeError('Only one yield in fixture is allowed') class SyncFixtureWithAsyncDeps(BaseAsyncFixture): @@ -167,14 +168,15 @@ async def _setup(self, resolved_deps): __tracebackhide__ = True gen = self.fixturedef.func(**resolved_deps) - await yield_(gen.send(None)) - try: - gen.send(None) - except StopIteration: - pass - else: - raise RuntimeError('Only one yield in fixture is allowed') + await yield_(gen.send(None)) + finally: + try: + gen.send(None) + except StopIteration: + pass + else: + raise RuntimeError('Only one yield in fixture is allowed') class AsyncFixture(BaseAsyncFixture): From f6b3739a8e8019666f271f1b175fec1853121c95 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Sat, 24 Mar 2018 10:42:38 +0100 Subject: [PATCH 014/185] Fix style --- pytest_trio/_tests/test_async_yield_fixture.py | 4 +++- pytest_trio/_tests/test_sync_fixture.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pytest_trio/_tests/test_async_yield_fixture.py b/pytest_trio/_tests/test_async_yield_fixture.py index f7c7b08..757769f 100644 --- a/pytest_trio/_tests/test_async_yield_fixture.py +++ b/pytest_trio/_tests/test_async_yield_fixture.py @@ -308,4 +308,6 @@ def test_after(): result = testdir.runpytest() result.assert_outcomes(failed=1, passed=2) - result.stdout.re_match_lines([r'E\W+RuntimeError: Crash during fixture teardown']) + result.stdout.re_match_lines( + [r'E\W+RuntimeError: Crash during fixture teardown'] + ) diff --git a/pytest_trio/_tests/test_sync_fixture.py b/pytest_trio/_tests/test_sync_fixture.py index a525f98..877da28 100644 --- a/pytest_trio/_tests/test_sync_fixture.py +++ b/pytest_trio/_tests/test_sync_fixture.py @@ -135,4 +135,6 @@ def test_after(): result = testdir.runpytest() result.assert_outcomes(failed=1, passed=2) - result.stdout.re_match_lines([r'E\W+RuntimeError: Crash during fixture teardown']) + result.stdout.re_match_lines( + [r'E\W+RuntimeError: Crash during fixture teardown'] + ) From 21b5625db56df56aff56c393d58bb0ceb1a97d62 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Sat, 24 Mar 2018 10:43:02 +0100 Subject: [PATCH 015/185] Make fixture resolution order determinist on Python < 3.6 --- pytest_trio/plugin.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index b8b36b5..4246184 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -1,20 +1,24 @@ """pytest-trio implementation.""" +import sys from traceback import format_exception from inspect import iscoroutinefunction, isgeneratorfunction -try: - from inspect import isasyncgenfunction -except ImportError: - # `inspect.isasyncgenfunction` not available with Python<3.6 - def isasyncgenfunction(x): - return False - - import pytest import trio from trio._util import acontextmanager from trio.testing import MockClock, trio_test from async_generator import async_generator, yield_ +if sys.version_info >= (3, 6): + from inspect import isasyncgenfunction + ORDERED_DICTS = True +else: + # `inspect.isasyncgenfunction` not available with Python<3.6 + def isasyncgenfunction(x): + return False + + # Ordered dict (and **kwargs) not available with Python<3.6 + ORDERED_DICTS = False + def pytest_configure(config): """Inject documentation.""" @@ -69,6 +73,9 @@ async def _setup_async_fixtures_in(deps): need_resolved_deps_stack = [ (k, v) for k, v in deps.items() if isinstance(v, BaseAsyncFixture) ] + if not ORDERED_DICTS: + # Make the fixture resolution order determinist + need_resolved_deps_stack = sorted(need_resolved_deps_stack) if not need_resolved_deps_stack: await yield_(deps) From eefd8f5eb5d9026d8b135f7d88aa11f541b8d8fd Mon Sep 17 00:00:00 2001 From: Matthias Urlichs Date: Tue, 27 Mar 2018 00:10:17 +0200 Subject: [PATCH 016/185] Use async_generator's isasyncgenfunction This adds Python 3.5 compatibility for async fixtures. --- pytest_trio/plugin.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 813b1d1..f48c35b 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -2,12 +2,9 @@ from traceback import format_exception from inspect import iscoroutinefunction, isgeneratorfunction try: - from inspect import isasyncgenfunction + from async_generator import isasyncgenfunction except ImportError: - # `inspect.isasyncgenfunction` not available with Python<3.6 - def isasyncgenfunction(x): - return False - + from inspect import isasyncgenfunction import pytest import trio From 735e3c79c48e42ee80c39d1fba9a455659e84b74 Mon Sep 17 00:00:00 2001 From: Matthias Urlichs Date: Tue, 3 Apr 2018 17:31:44 +0200 Subject: [PATCH 017/185] Use async_generator's asynccontextmanager --- pytest_trio/plugin.py | 17 ++++++++--------- setup.py | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index f48c35b..53d0bf3 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -8,9 +8,8 @@ import pytest import trio -from trio._util import acontextmanager from trio.testing import MockClock, trio_test -from async_generator import async_generator, yield_ +from async_generator import async_generator, yield_, asynccontextmanager def pytest_configure(config): @@ -58,7 +57,7 @@ async def _bootstrap_fixture_and_run_test(**kwargs): return _bootstrap_fixture_and_run_test -@acontextmanager +@asynccontextmanager @async_generator async def _setup_async_fixtures_in(deps): __tracebackhide__ = True @@ -71,7 +70,7 @@ async def _setup_async_fixtures_in(deps): await yield_(deps) return - @acontextmanager + @asynccontextmanager @async_generator async def _recursive_setup(deps_stack): __tracebackhide__ = True @@ -104,7 +103,7 @@ def __init__(self, fixturedef, deps={}): self.setup_done = False self.result = None - @acontextmanager + @asynccontextmanager @async_generator async def setup(self): __tracebackhide__ = True @@ -125,7 +124,7 @@ class AsyncYieldFixture(BaseAsyncFixture): Async generator fixture. """ - @acontextmanager + @asynccontextmanager @async_generator async def _setup(self, resolved_deps): __tracebackhide__ = True @@ -146,7 +145,7 @@ class SyncFixtureWithAsyncDeps(BaseAsyncFixture): Synchronous function fixture with asynchronous dependencies fixtures. """ - @acontextmanager + @asynccontextmanager @async_generator async def _setup(self, resolved_deps): __tracebackhide__ = True @@ -158,7 +157,7 @@ class SyncYieldFixtureWithAsyncDeps(BaseAsyncFixture): Synchronous generator fixture with asynchronous dependencies fixtures. """ - @acontextmanager + @asynccontextmanager @async_generator async def _setup(self, resolved_deps): __tracebackhide__ = True @@ -179,7 +178,7 @@ class AsyncFixture(BaseAsyncFixture): Regular async fixture (i.e. coroutine). """ - @acontextmanager + @asynccontextmanager @async_generator async def _setup(self, resolved_deps): __tracebackhide__ = True diff --git a/setup.py b/setup.py index 0fb7934..03a577e 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ entry_points={'pytest11': ['trio = pytest_trio.plugin']}, install_requires=[ "trio", - "async_generator >= 1.6", + "async_generator >= 1.9", ], keywords=[ 'async', From 11a005f5cc5b8eca2d5a1ce5890decb114aa2561 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Wed, 11 Apr 2018 18:46:18 +0200 Subject: [PATCH 018/185] Improve test_async_yield_fixture.py by also testing against async_generator module --- .../_tests/test_async_yield_fixture.py | 111 +++++++++++++----- 1 file changed, 80 insertions(+), 31 deletions(-) diff --git a/pytest_trio/_tests/test_async_yield_fixture.py b/pytest_trio/_tests/test_async_yield_fixture.py index 757769f..b46dd06 100644 --- a/pytest_trio/_tests/test_async_yield_fixture.py +++ b/pytest_trio/_tests/test_async_yield_fixture.py @@ -1,23 +1,43 @@ import sys import pytest +import re -@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6") -def test_single_async_yield_fixture(testdir): +@pytest.fixture(params=['Python>=36', 'async_generator']) +def async_yield_implementation(request): + if request.param == 'Python>=36': + if sys.version_info < (3, 6): + pytest.skip("requires python3.6") + else: + def patch_code(code): + # Convert code to use Python>=3.6 builtin async generator + code = re.sub(r'(?m)^\s*@async_generator\n', r'', code) + code = re.sub(r'await yield_', r'yield', code) + return code + + return patch_code + else: + return lambda x: x + + +def test_single_async_yield_fixture(testdir, async_yield_implementation): testdir.makepyfile( - """ + async_yield_implementation( + """ import pytest import trio + from async_generator import async_generator, yield_ events = [] @pytest.fixture + @async_generator async def fix1(): events.append('fix1 setup') await trio.sleep(0) - yield 'fix1' + await yield_('fix1') await trio.sleep(0) events.append('fix1 teardown') @@ -36,6 +56,7 @@ def test_after(): 'fix1 teardown', ] """ + ) ) result = testdir.runpytest() @@ -43,32 +64,35 @@ def test_after(): result.assert_outcomes(passed=3) -@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6") -def test_nested_async_yield_fixture(testdir): +def test_nested_async_yield_fixture(testdir, async_yield_implementation): testdir.makepyfile( - """ + async_yield_implementation( + """ import pytest import trio + from async_generator import async_generator, yield_ events = [] @pytest.fixture + @async_generator async def fix2(): events.append('fix2 setup') await trio.sleep(0) - yield 'fix2' + await yield_('fix2') await trio.sleep(0) events.append('fix2 teardown') @pytest.fixture + @async_generator async def fix1(fix2): events.append('fix1 setup') await trio.sleep(0) - yield 'fix1' + await yield_('fix1') await trio.sleep(0) events.append('fix1 teardown') @@ -92,6 +116,7 @@ def test_after(): 'fix2 teardown', ] """ + ) ) result = testdir.runpytest() @@ -99,22 +124,26 @@ def test_after(): result.assert_outcomes(passed=3) -@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6") -def test_async_yield_fixture_within_sync_fixture(testdir): +def test_async_yield_fixture_within_sync_fixture( + testdir, async_yield_implementation +): testdir.makepyfile( - """ + async_yield_implementation( + """ import pytest import trio + from async_generator import async_generator, yield_ events = [] @pytest.fixture + @async_generator async def fix2(): events.append('fix2 setup') await trio.sleep(0) - yield 'fix2' + await yield_('fix2') await trio.sleep(0) events.append('fix2 teardown') @@ -139,6 +168,7 @@ def test_after(): 'fix2 teardown', ] """ + ) ) result = testdir.runpytest() @@ -146,22 +176,26 @@ def test_after(): result.assert_outcomes(passed=3) -@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6") -def test_async_yield_fixture_within_sync_yield_fixture(testdir): +def test_async_yield_fixture_within_sync_yield_fixture( + testdir, async_yield_implementation +): testdir.makepyfile( - """ + async_yield_implementation( + """ import pytest import trio + from async_generator import async_generator, yield_ events = [] @pytest.fixture + @async_generator async def fix2(): events.append('fix2 setup') await trio.sleep(0) - yield 'fix2' + await yield_('fix2') await trio.sleep(0) events.append('fix2 teardown') @@ -191,6 +225,7 @@ def test_after(): 'fix2 teardown', ] """ + ) ) result = testdir.runpytest() @@ -198,25 +233,30 @@ def test_after(): result.assert_outcomes(passed=3) -@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6") -def test_async_yield_fixture_with_multiple_yields(testdir): +def test_async_yield_fixture_with_multiple_yields( + testdir, async_yield_implementation +): testdir.makepyfile( - """ + async_yield_implementation( + """ import pytest import trio + from async_generator import async_generator, yield_ @pytest.fixture + @async_generator async def fix1(): await trio.sleep(0) - yield 'good' + await yield_('good') await trio.sleep(0) - yield 'bad' + await yield_('bad') @pytest.mark.trio async def test_actual_test(fix1): pass """ + ) ) result = testdir.runpytest() @@ -226,13 +266,14 @@ async def test_actual_test(fix1): result.assert_outcomes(failed=1) -@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6") -def test_async_yield_fixture_with_nursery(testdir): +def test_async_yield_fixture_with_nursery(testdir, async_yield_implementation): testdir.makepyfile( - """ + async_yield_implementation( + """ import pytest import trio + from async_generator import async_generator, yield_ async def handle_client(stream): @@ -242,10 +283,11 @@ async def handle_client(stream): @pytest.fixture + @async_generator async def server(): async with trio.open_nursery() as nursery: listeners = await nursery.start(trio.serve_tcp, handle_client, 0) - yield listeners[0] + await yield_(listeners[0]) nursery.cancel_scope.cancel() @@ -256,6 +298,7 @@ async def test_actual_test(server): rep = await stream.receive_some(4) assert rep == b'ping' """ + ) ) result = testdir.runpytest() @@ -263,28 +306,33 @@ async def test_actual_test(server): result.assert_outcomes(passed=1) -@pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6") -def test_async_yield_fixture_crashed_teardown_allow_other_teardowns(testdir): +def test_async_yield_fixture_crashed_teardown_allow_other_teardowns( + testdir, async_yield_implementation +): testdir.makepyfile( - """ + async_yield_implementation( + """ import pytest import trio + from async_generator import async_generator, yield_ events = [] @pytest.fixture + @async_generator async def good_fixture(): async with trio.open_nursery() as nursery: events.append('good_fixture setup') - yield + await yield_(None) events.append('good_fixture teardown') @pytest.fixture + @async_generator async def bad_fixture(): async with trio.open_nursery() as nursery: events.append('bad_fixture setup') - yield + await yield_(None) events.append('bad_fixture teardown') raise RuntimeError('Crash during fixture teardown') @@ -303,6 +351,7 @@ def test_after(): 'good_fixture teardown', ] """ + ) ) result = testdir.runpytest() From d0df65b49819aa71de56cae56d2b939b8dc3e874 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Sat, 14 Apr 2018 16:09:42 +0200 Subject: [PATCH 019/185] Disable CI on macOS/Python3.5 (TLSv1.2 required by pip is not available) --- .travis.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 271a7c9..c37a900 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,9 +13,11 @@ matrix: - os: linux language: generic env: USE_PYPY_RELEASE_VERSION=5.9-beta - - os: osx - language: generic - env: MACPYTHON=3.5.4 + # Python3.5 on MacOS doesn't currently support TLSv1.2 needed to use pip :( + # http://pyfound.blogspot.fr/2017/01/time-to-upgrade-your-python-tls-v12.html + # - os: osx + # language: generic + # env: MACPYTHON=3.5.4 - os: osx language: generic env: MACPYTHON=3.6.3 From 1c1f50b144ab8ff1a6b543acc0cae1556616ca1a Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Sat, 14 Apr 2018 16:29:21 +0200 Subject: [PATCH 020/185] Bumpversion 0.3.0 -> 0.4.0 --- docs/source/history.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/source/history.rst b/docs/source/history.rst index d1ff8a3..218102c 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -5,6 +5,13 @@ Release history .. towncrier release notes start +Pytest_Trio 0.4.0 (2018-04-14) +------------------------------ + +- Fix compatibility with trio 0.4.0 (`#25 + `__) + + Pytest_Trio 0.3.0 (2018-01-03) ------------------------------ From 0759521358b2e084ddd6ae7ef4869a3630f9ce56 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Sat, 14 Apr 2018 16:51:37 +0200 Subject: [PATCH 021/185] Bumpversion 0.4.0 -> 0.4.1 --- docs/source/history.rst | 6 ++++++ pytest_trio/_version.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/source/history.rst b/docs/source/history.rst index 218102c..bdf268f 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -5,6 +5,12 @@ Release history .. towncrier release notes start +Pytest_Trio 0.4.1 (2018-04-14) +------------------------------ + +No significant changes. + + Pytest_Trio 0.4.0 (2018-04-14) ------------------------------ diff --git a/pytest_trio/_version.py b/pytest_trio/_version.py index f879b44..48f8f2b 100644 --- a/pytest_trio/_version.py +++ b/pytest_trio/_version.py @@ -1,3 +1,3 @@ # This file is imported from __init__.py and exec'd from setup.py -__version__ = "0.3.0" +__version__ = "0.4.1" From ccd5ec28b8e0e84a024e0f13ed4fa79235bfc5aa Mon Sep 17 00:00:00 2001 From: Zac-HD Date: Wed, 27 Jun 2018 23:30:07 +1000 Subject: [PATCH 022/185] Support Trio testing with Hypothesis --- .gitignore | 2 ++ newsfragments/42.feature.rst | 2 ++ .../_tests/test_hypothesis_interaction.py | 22 +++++++++++++++ pytest_trio/plugin.py | 28 +++++++++++++++---- test-requirements.txt | 1 + 5 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 newsfragments/42.feature.rst create mode 100644 pytest_trio/_tests/test_hypothesis_interaction.py diff --git a/.gitignore b/.gitignore index afdfef3..79d6635 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,8 @@ htmlcov/ .cache nosetests.xml coverage.xml +.hypothesis/ +.pytest_cache/ # Translations *.mo diff --git a/newsfragments/42.feature.rst b/newsfragments/42.feature.rst new file mode 100644 index 0000000..18f4ff2 --- /dev/null +++ b/newsfragments/42.feature.rst @@ -0,0 +1,2 @@ +pytest-trio now integrates with `Hypothesis `_ +to support ``@given`` on async tests using Trio. \ No newline at end of file diff --git a/pytest_trio/_tests/test_hypothesis_interaction.py b/pytest_trio/_tests/test_hypothesis_interaction.py new file mode 100644 index 0000000..6803d1e --- /dev/null +++ b/pytest_trio/_tests/test_hypothesis_interaction.py @@ -0,0 +1,22 @@ +import pytest +from hypothesis import given, strategies as st + + +@given(st.integers()) +@pytest.mark.trio +async def test_mark_inner(n): + assert isinstance(n, int) + + +@pytest.mark.trio +@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.trio +async def test_mark_and_parametrize(x, y): + assert x is None + assert y in (1, 2) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 5d55a3b..29ed325 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -25,8 +25,18 @@ def pytest_configure(config): ) -def _trio_test_runner_factory(item): - testfunc = item.function +def _trio_test_runner_factory(item, testfunc=None): + testfunc = testfunc or item.function + + if getattr(testfunc, '_trio_test_runner_wrapped', False): + # We have already wrapped this, perhaps because we combined Hypothesis + # with pytest.mark.parametrize + return testfunc + + if not iscoroutinefunction(testfunc): + pytest.fail( + 'test function `%r` is marked trio but is not async' % item + ) @trio_test async def _bootstrap_fixture_and_run_test(**kwargs): @@ -58,6 +68,7 @@ async def _bootstrap_fixture_and_run_test(**kwargs): if user_exc: raise user_exc + _bootstrap_fixture_and_run_test._trio_test_runner_wrapped = True return _bootstrap_fixture_and_run_test @@ -215,11 +226,18 @@ def _install_async_fixture_if_needed(fixturedef, request): @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(item): if 'trio' in item.keywords: - if not iscoroutinefunction(item.obj): + if hasattr(item.obj, 'hypothesis'): + # If it's a Hypothesis test, we go in a layer. + item.obj.hypothesis.inner_test = _trio_test_runner_factory( + item, item.obj.hypothesis.inner_test + ) + elif getattr(item.obj, 'is_hypothesis_test', False): pytest.fail( - 'test function `%r` is marked trio but is not async' % item + 'test function `%r` is using Hypothesis, but pytest-trio ' + 'only works with Hypothesis 3.64.0 or later.' % item ) - item.obj = _trio_test_runner_factory(item) + else: + item.obj = _trio_test_runner_factory(item) yield diff --git a/test-requirements.txt b/test-requirements.txt index 9955dec..ffdc000 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,2 +1,3 @@ pytest pytest-cov +hypothesis>=3.64 From 8d1526ac0b978efc73547fd85048db9add6472f3 Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Fri, 29 Jun 2018 10:26:26 +0200 Subject: [PATCH 023/185] Bump version 0.4.1 -> 0.4.2 --- docs/source/history.rst | 11 +++++++++++ newsfragments/42.feature.rst | 2 -- pytest_trio/_version.py | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) delete mode 100644 newsfragments/42.feature.rst diff --git a/docs/source/history.rst b/docs/source/history.rst index bdf268f..38c7930 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -5,6 +5,17 @@ Release history .. towncrier release notes start +Pytest_Trio 0.4.2 (2018-06-29) +------------------------------ + +Features +~~~~~~~~ + +- pytest-trio now integrates with `Hypothesis + `_ to support ``@given`` on async tests + using Trio. (`#42 `__) + + Pytest_Trio 0.4.1 (2018-04-14) ------------------------------ diff --git a/newsfragments/42.feature.rst b/newsfragments/42.feature.rst deleted file mode 100644 index 18f4ff2..0000000 --- a/newsfragments/42.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -pytest-trio now integrates with `Hypothesis `_ -to support ``@given`` on async tests using Trio. \ No newline at end of file diff --git a/pytest_trio/_version.py b/pytest_trio/_version.py index 48f8f2b..3c33369 100644 --- a/pytest_trio/_version.py +++ b/pytest_trio/_version.py @@ -1,3 +1,3 @@ # This file is imported from __init__.py and exec'd from setup.py -__version__ = "0.4.1" +__version__ = "0.4.2" From 55351c57cbfabd9d0ef820287e87e6f9acbf31c7 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Sat, 21 Jul 2018 20:48:00 -0700 Subject: [PATCH 024/185] Add more detail to comment --- pytest_trio/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 29ed325..b0a4c01 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -17,7 +17,7 @@ def pytest_configure(config): - """Inject documentation.""" + # So that it shows up in 'pytest --markers' output: config.addinivalue_line( "markers", "trio: " "mark the test as an async trio test; " From a27b5765b42c401f03684e3132938fb3343d95fa Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Sat, 21 Jul 2018 20:48:50 -0700 Subject: [PATCH 025/185] Use any() in more idiomatic way --- pytest_trio/plugin.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index b0a4c01..0a04eb0 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -212,8 +212,7 @@ def _install_async_fixture_if_needed(fixturedef, request): asyncfix = AsyncFixture(fixturedef, deps) elif isasyncgenfunction(fixturedef.func): asyncfix = AsyncYieldFixture(fixturedef, deps) - elif any(dep for dep in deps.values() - if isinstance(dep, BaseAsyncFixture)): + elif any(isinstance(dep, BaseAsyncFixture) for dep in deps.values()): if isgeneratorfunction(fixturedef.func): asyncfix = SyncYieldFixtureWithAsyncDeps(fixturedef, deps) else: From 002bec793d510dfccaf3933bdbc4d1bc54b2b144 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Sat, 21 Jul 2018 20:50:05 -0700 Subject: [PATCH 026/185] Upgrade to yapf 0.22.0 --- ci/travis.sh | 2 +- pytest_trio/plugin.py | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/ci/travis.sh b/ci/travis.sh index a59d32f..22230d3 100755 --- a/ci/travis.sh +++ b/ci/travis.sh @@ -3,7 +3,7 @@ set -ex # See https://github.com/python-trio/trio/issues/334 -YAPF_VERSION=0.17.0 +YAPF_VERSION=0.22.0 if [ "$TRAVIS_OS_NAME" = "osx" ]; then curl -Lo macpython.pkg https://www.python.org/ftp/python/${MACPYTHON}/python-${MACPYTHON}-macosx10.6.pkg diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 0a04eb0..d07fa84 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -97,16 +97,14 @@ async def _recursive_setup(deps_stack): if not deps_stack: await yield_([(name, resolved)]) else: - async with _recursive_setup( - deps_stack - ) as remains_deps_stack_resolved: + async with _recursive_setup(deps_stack + ) as remains_deps_stack_resolved: await yield_( remains_deps_stack_resolved + [(name, resolved)] ) - async with _recursive_setup( - need_resolved_deps_stack - ) as resolved_deps_stack: + async with _recursive_setup(need_resolved_deps_stack + ) as resolved_deps_stack: await yield_({**deps, **dict(resolved_deps_stack)}) From 8153229cdc68ac49846165a1be8975d09df011aa Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Sat, 21 Jul 2018 20:53:19 -0700 Subject: [PATCH 027/185] Organize code a bit (no semantic changes) --- pytest_trio/plugin.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index d07fa84..fdf1a66 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -9,6 +9,10 @@ async_generator, yield_, asynccontextmanager, isasyncgenfunction ) +################################################################ +# Basic setup +################################################################ + if sys.version_info >= (3, 6): ORDERED_DICTS = True else: @@ -25,6 +29,18 @@ def pytest_configure(config): ) +@pytest.hookimpl(tryfirst=True) +def pytest_exception_interact(node, call, report): + if issubclass(call.excinfo.type, trio.MultiError): + # TODO: not really elegant (pytest cannot output color with this hack) + report.longrepr = ''.join(format_exception(*call.excinfo._excinfo)) + + +################################################################ +# Core support for running tests and constructing fixtures +################################################################ + + def _trio_test_runner_factory(item, testfunc=None): testfunc = testfunc or item.function @@ -245,11 +261,9 @@ def pytest_fixture_setup(fixturedef, request): return _install_async_fixture_if_needed(fixturedef, request) -@pytest.hookimpl(tryfirst=True) -def pytest_exception_interact(node, call, report): - if issubclass(call.excinfo.type, trio.MultiError): - # TODO: not really elegant (pytest cannot output color with this hack) - report.longrepr = ''.join(format_exception(*call.excinfo._excinfo)) +################################################################ +# Built-in fixtures +################################################################ @pytest.fixture From 4f50bf4db06ca81ec01e31852b4306a0d4d410f0 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Sat, 21 Jul 2018 20:54:42 -0700 Subject: [PATCH 028/185] Rewrap comments to 80ish columns --- pytest_trio/plugin.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index fdf1a66..68632c7 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -67,7 +67,8 @@ async def _bootstrap_fixture_and_run_test(**kwargs): await testfunc(**resolved_kwargs) except BaseException as exc: # Regular pytest fixture don't have access to the test - # exception in there teardown, we mimic this behavior here. + # exception in there teardown, we mimic this behavior + # here. user_exc = exc except BaseException as exc: # If we are here, the exception comes from the fixtures setup @@ -77,10 +78,12 @@ async def _bootstrap_fixture_and_run_test(**kwargs): else: raise exc finally: - # No matter what the nursery fixture should be closed when test is over + # No matter what the nursery fixture should be closed when + # test is over nursery.cancel_scope.cancel() - # Finally re-raise or original exception coming from the test if needed + # Finally re-raise or original exception coming from the test if + # needed if user_exc: raise user_exc From 61a72bcd336fff4182875ddf48a068c305c27b67 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Sun, 22 Jul 2018 23:52:56 -0700 Subject: [PATCH 029/185] Disable hypothesis deadline to avoid random CI failures --- pytest_trio/_tests/test_hypothesis_interaction.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pytest_trio/_tests/test_hypothesis_interaction.py b/pytest_trio/_tests/test_hypothesis_interaction.py index 6803d1e..2cf969f 100644 --- a/pytest_trio/_tests/test_hypothesis_interaction.py +++ b/pytest_trio/_tests/test_hypothesis_interaction.py @@ -1,19 +1,26 @@ import pytest -from hypothesis import given, strategies as st +from hypothesis import given, settings, strategies as st +# To avoid unpredictable warnings/errors when CI happens to be slow +# Example: https://travis-ci.org/python-trio/pytest-trio/jobs/406738296 +our_settings = settings(deadline=None) + +@our_settings @given(st.integers()) @pytest.mark.trio async def test_mark_inner(n): assert isinstance(n, int) +@our_settings @pytest.mark.trio @given(st.integers()) async def test_mark_outer(n): assert isinstance(n, int) +@our_settings @pytest.mark.parametrize('y', [1, 2]) @given(x=st.none()) @pytest.mark.trio From 910d79c62194260bc9ec9f05e060806e5748f25f Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Sun, 22 Jul 2018 23:48:42 -0700 Subject: [PATCH 030/185] Implement Trio mode Fixes gh-9 Documentation in gh-47 --- .../_tests/test_hypothesis_interaction.py | 7 +-- pytest_trio/_tests/test_trio_mode.py | 44 +++++++++++++++++++ pytest_trio/enable_trio_mode.py | 7 +++ pytest_trio/plugin.py | 29 ++++++++++++ 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 pytest_trio/_tests/test_trio_mode.py create mode 100644 pytest_trio/enable_trio_mode.py diff --git a/pytest_trio/_tests/test_hypothesis_interaction.py b/pytest_trio/_tests/test_hypothesis_interaction.py index 2cf969f..26fe612 100644 --- a/pytest_trio/_tests/test_hypothesis_interaction.py +++ b/pytest_trio/_tests/test_hypothesis_interaction.py @@ -1,9 +1,10 @@ import pytest from hypothesis import given, settings, strategies as st -# To avoid unpredictable warnings/errors when CI happens to be slow -# Example: https://travis-ci.org/python-trio/pytest-trio/jobs/406738296 -our_settings = settings(deadline=None) +# deadline=None avoids unpredictable warnings/errors when CI happens to be +# slow (example: https://travis-ci.org/python-trio/pytest-trio/jobs/406738296) +# max_examples=5 speeds things up a bit +our_settings = settings(deadline=None, max_examples=5) @our_settings diff --git a/pytest_trio/_tests/test_trio_mode.py b/pytest_trio/_tests/test_trio_mode.py new file mode 100644 index 0000000..764f5da --- /dev/null +++ b/pytest_trio/_tests/test_trio_mode.py @@ -0,0 +1,44 @@ +import pytest + +test_text = """ +import pytest +import trio +from hypothesis import given, settings, strategies + +async def test_pass(): + await trio.sleep(0) + +async def test_fail(): + await trio.sleep(0) + assert False + +@settings(deadline=None, max_examples=5) +@given(strategies.binary()) +async def test_hypothesis_pass(b): + await trio.sleep(0) + assert isinstance(b, bytes) + +@settings(deadline=None, max_examples=5) +@given(strategies.binary()) +async def test_hypothesis_fail(b): + await trio.sleep(0) + assert isinstance(b, int) +""" + + +def test_trio_mode_pytest_ini(testdir): + testdir.makepyfile(test_text) + + testdir.makefile(".ini", pytest="[pytest]\ntrio_mode = true\n") + + result = testdir.runpytest() + result.assert_outcomes(passed=2, failed=2) + + +def test_trio_mode_conftest(testdir): + testdir.makepyfile(test_text) + + testdir.makeconftest("from pytest_trio.enable_trio_mode import *") + + result = testdir.runpytest() + result.assert_outcomes(passed=2, failed=2) diff --git a/pytest_trio/enable_trio_mode.py b/pytest_trio/enable_trio_mode.py new file mode 100644 index 0000000..6f05933 --- /dev/null +++ b/pytest_trio/enable_trio_mode.py @@ -0,0 +1,7 @@ +__all__ = ["pytest_collection_modifyitems"] + +from .plugin import automark + + +def pytest_collection_modifyitems(items): + automark(items) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 68632c7..59d79e0 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -20,6 +20,15 @@ ORDERED_DICTS = False +def pytest_addoption(parser): + parser.addini( + "trio_mode", + "should pytest-trio handle all async functions?", + type="bool", + default=False, + ) + + def pytest_configure(config): # So that it shows up in 'pytest --markers' output: config.addinivalue_line( @@ -264,6 +273,26 @@ def pytest_fixture_setup(fixturedef, request): return _install_async_fixture_if_needed(fixturedef, request) +################################################################ +# Trio mode +################################################################ + + +def automark(items): + for item in items: + if hasattr(item.obj, "hypothesis"): + test_func = item.obj.hypothesis.inner_test + else: + test_func = item.obj + if iscoroutinefunction(test_func): + item.add_marker(pytest.mark.trio) + + +def pytest_collection_modifyitems(config, items): + if config.getini("trio_mode"): + automark(items) + + ################################################################ # Built-in fixtures ################################################################ From 2036d760c081eee0ec27584fb6dda284dc345d79 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Mon, 23 Jul 2018 22:46:11 -0700 Subject: [PATCH 031/185] Rework fixtures - Add @pytest_trio.trio_fixture for explicitly marking a fixture as being a trio fixture - Make the nursery fixture a @trio_fixture - Refactor Trio fixture classes into one class - Check for trio marker instead of trio keyword (fixes gh-43) - This also raises the minimum pytest version to 3.6 - Raise an error if a Trio fixture is used with a non-function scope (fixes gh-18) - Raise an error if a Trio fixture is used with a non-Trio test I think this also closes gh-10's discussion, though we still need to convince pytest-asyncio to fix their side of things. --- pytest_trio/__init__.py | 3 + pytest_trio/_tests/test_fixture_mistakes.py | 68 ++++++++ pytest_trio/plugin.py | 164 ++++++++------------ setup.py | 2 + 4 files changed, 138 insertions(+), 99 deletions(-) create mode 100644 pytest_trio/_tests/test_fixture_mistakes.py diff --git a/pytest_trio/__init__.py b/pytest_trio/__init__.py index ffd3b89..5fbd91f 100644 --- a/pytest_trio/__init__.py +++ b/pytest_trio/__init__.py @@ -1,3 +1,6 @@ """Top-level package for pytest-trio.""" from ._version import __version__ +from .plugin import trio_fixture + +__all__ = ["trio_fixture"] diff --git a/pytest_trio/_tests/test_fixture_mistakes.py b/pytest_trio/_tests/test_fixture_mistakes.py new file mode 100644 index 0000000..2e775f2 --- /dev/null +++ b/pytest_trio/_tests/test_fixture_mistakes.py @@ -0,0 +1,68 @@ +import pytest +from pytest_trio import trio_fixture + + +def test_trio_fixture_with_non_trio_test(testdir): + testdir.makepyfile( + """ + import trio + from pytest_trio import trio_fixture + import pytest + + @trio_fixture + def trio_time(): + return trio.current_time() + + @pytest.fixture + def indirect_trio_time(trio_time): + return trio_time + 1 + + @pytest.mark.trio + async def test_async(mock_clock, trio_time, indirect_trio_time): + assert trio_time == 0 + assert indirect_trio_time == 1 + + def test_sync(trio_time): + pass + + def test_sync_indirect(indirect_trio_time): + pass + """ + ) + + result = testdir.runpytest() + + result.assert_outcomes(passed=1, error=2) + result.stdout.fnmatch_lines( + ["*Trio fixtures can only be used by Trio tests*"] + ) + + +def test_trio_fixture_with_wrong_scope(testdir): + # There's a trick here: when you have a non-function-scope fixture, it's + # not instantiated for any particular function (obviously). So... when our + # pytest_fixture_setup hook tries to check for marks, it can't normally + # see @pytest.mark.trio. So... it's actually almost impossible to have an + # async fixture get treated as a Trio fixture *and* have it be + # non-function-scope. But, class-scoped fixtures can see marks on the + # class, so this is one way (the only way?) it can happen: + testdir.makepyfile( + """ + import pytest + import pytest_trio + + @pytest.fixture(scope="class") + async def async_class_fixture(): + pass + + @pytest.mark.trio + class TestFoo: + async def test_foo(self, async_class_fixture): + pass + """ + ) + + result = testdir.runpytest() + + result.assert_outcomes(error=1) + result.stdout.fnmatch_lines(["*must be function-scope*"]) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 59d79e0..95d95d8 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -1,12 +1,14 @@ """pytest-trio implementation.""" import sys from traceback import format_exception +from collections.abc import Coroutine, Generator from inspect import iscoroutinefunction, isgeneratorfunction import pytest import trio from trio.testing import MockClock, trio_test from async_generator import ( - async_generator, yield_, asynccontextmanager, isasyncgenfunction + async_generator, yield_, asynccontextmanager, isasyncgen, + isasyncgenfunction ) ################################################################ @@ -106,7 +108,7 @@ async def _setup_async_fixtures_in(deps): __tracebackhide__ = True need_resolved_deps_stack = [ - (k, v) for k, v in deps.items() if isinstance(v, BaseAsyncFixture) + (k, v) for k, v in deps.items() if isinstance(v, TrioFixture) ] if not ORDERED_DICTS: # Make the fixture resolution order determinist @@ -136,7 +138,7 @@ async def _recursive_setup(deps_stack): await yield_({**deps, **dict(resolved_deps_stack)}) -class BaseAsyncFixture: +class TrioFixture: """ Represent a fixture that need to be run in a trio context to be resolved. """ @@ -155,102 +157,39 @@ async def setup(self): await yield_(self.result) else: async with _setup_async_fixtures_in(self.deps) as resolved_deps: - async with self._setup(resolved_deps) as self.result: - self.setup_done = True - await yield_(self.result) - - async def _setup(self): - raise NotImplementedError() - - -class AsyncYieldFixture(BaseAsyncFixture): - """ - Async generator fixture. - """ - - @asynccontextmanager - @async_generator - async def _setup(self, resolved_deps): - __tracebackhide__ = True - agen = self.fixturedef.func(**resolved_deps) - - try: - await yield_(await agen.asend(None)) - finally: - try: - await agen.asend(None) - except StopAsyncIteration: - pass - else: - raise RuntimeError('Only one yield in fixture is allowed') - - -class SyncFixtureWithAsyncDeps(BaseAsyncFixture): - """ - Synchronous function fixture with asynchronous dependencies fixtures. - """ - - @asynccontextmanager - @async_generator - async def _setup(self, resolved_deps): - __tracebackhide__ = True - await yield_(self.fixturedef.func(**resolved_deps)) - - -class SyncYieldFixtureWithAsyncDeps(BaseAsyncFixture): - """ - Synchronous generator fixture with asynchronous dependencies fixtures. - """ - - @asynccontextmanager - @async_generator - async def _setup(self, resolved_deps): - __tracebackhide__ = True - gen = self.fixturedef.func(**resolved_deps) - - try: - await yield_(gen.send(None)) - finally: - try: - gen.send(None) - except StopIteration: - pass - else: - raise RuntimeError('Only one yield in fixture is allowed') - - -class AsyncFixture(BaseAsyncFixture): - """ - Regular async fixture (i.e. coroutine). - """ - - @asynccontextmanager - @async_generator - async def _setup(self, resolved_deps): - __tracebackhide__ = True - await yield_(await self.fixturedef.func(**resolved_deps)) - + retval = self.fixturedef.func(**resolved_deps) + if isinstance(retval, Coroutine): + self.result = await retval + elif isasyncgen(retval): + self.result = await retval.asend(None) + elif isinstance(retval, Generator): + self.result = retval.send(None) + else: + # Regular synchronous function + self.result = retval -def _install_async_fixture_if_needed(fixturedef, request): - asyncfix = None - deps = {dep: request.getfixturevalue(dep) for dep in fixturedef.argnames} - if iscoroutinefunction(fixturedef.func): - asyncfix = AsyncFixture(fixturedef, deps) - elif isasyncgenfunction(fixturedef.func): - asyncfix = AsyncYieldFixture(fixturedef, deps) - elif any(isinstance(dep, BaseAsyncFixture) for dep in deps.values()): - if isgeneratorfunction(fixturedef.func): - asyncfix = SyncYieldFixtureWithAsyncDeps(fixturedef, deps) - else: - asyncfix = SyncFixtureWithAsyncDeps(fixturedef, deps) - if asyncfix: - fixturedef.cached_result = (asyncfix, request.param_index, None) - return asyncfix + try: + await yield_(self.result) + finally: + if isasyncgen(retval): + try: + await retval.asend(None) + except StopAsyncIteration: + pass + else: + raise RuntimeError("too many yields in fixture") + elif isinstance(retval, Generator): + try: + retval.send(None) + except StopIteration: + pass + else: + raise RuntimeError("too many yields in fixture") @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(item): - if 'trio' in item.keywords: + if item.get_closest_marker("trio") is not None: if hasattr(item.obj, 'hypothesis'): # If it's a Hypothesis test, we go in a layer. item.obj.hypothesis.inner_test = _trio_test_runner_factory( @@ -267,10 +206,37 @@ def pytest_runtest_call(item): yield -@pytest.hookimpl() +# It's intentionally impossible to use this to create a non-function-scoped +# fixture (since that would require exposing a way to pass scope= to +# pytest.fixture). +def trio_fixture(func): + func._force_trio_fixture = True + return pytest.fixture(func) + + +def _is_trio_fixture(func, is_trio_test, deps): + if getattr(func, "_force_trio_fixture", False): + return True + if is_trio_test: + if iscoroutinefunction(func) or isasyncgenfunction(func): + return True + if any(isinstance(dep, TrioFixture) for dep in deps.values()): + return True + return False + + +@pytest.hookimpl def pytest_fixture_setup(fixturedef, request): - if 'trio' in request.keywords: - return _install_async_fixture_if_needed(fixturedef, request) + is_trio_test = (request.node.get_closest_marker("trio") is not None) + deps = {dep: request.getfixturevalue(dep) for dep in fixturedef.argnames} + if _is_trio_fixture(fixturedef.func, is_trio_test, deps): + if request.scope != "function": + raise RuntimeError("Trio fixtures must be function-scope") + if not is_trio_test: + raise RuntimeError("Trio fixtures can only be used by Trio tests") + fixture = TrioFixture(fixturedef, deps) + fixturedef.cached_result = (fixture, request.param_index, None) + return fixture ################################################################ @@ -308,6 +274,6 @@ def autojump_clock(): return MockClock(autojump_threshold=0) -@pytest.fixture -async def nursery(request): +@trio_fixture +def nursery(request): return request.node._trio_nursery diff --git a/setup.py b/setup.py index 03a577e..6b25497 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,8 @@ install_requires=[ "trio", "async_generator >= 1.9", + # For node.get_closest_marker + "pytest >= 3.6" ], keywords=[ 'async', From 82afdabed19e9b08f2004f566a0960da4e27a598 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Tue, 24 Jul 2018 01:00:32 -0700 Subject: [PATCH 032/185] Rename 'nursery' fixture to 'test_nursery' Every nursery is called 'nursery'. This is less generic, and emphasizes that this nursery is special, because its lifetime is linked to the test. Kept 'nursery' around for now as a deprecated alias. --- pytest_trio/_tests/test_nursery_fixture.py | 4 ++-- pytest_trio/plugin.py | 13 ++++++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/pytest_trio/_tests/test_nursery_fixture.py b/pytest_trio/_tests/test_nursery_fixture.py index 7719fa1..2a006d4 100644 --- a/pytest_trio/_tests/test_nursery_fixture.py +++ b/pytest_trio/_tests/test_nursery_fixture.py @@ -9,8 +9,8 @@ async def handle_client(stream): @pytest.fixture -async def server(nursery): - listeners = await nursery.start(trio.serve_tcp, handle_client, 0) +async def server(test_nursery): + listeners = await test_nursery.start(trio.serve_tcp, handle_client, 0) return listeners[0] diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 95d95d8..079b930 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -275,5 +275,16 @@ def autojump_clock(): @trio_fixture -def nursery(request): +def test_nursery(request): return request.node._trio_nursery + + +@trio_fixture +def nursery(test_nursery): + import warnings + warnings.warn( + FutureWarning( + "The 'nursery' fixture has been renamed to 'test_nursery'" + ) + ) + return test_nursery From 87fd1418b535bc80f5c3dc4cae3099802cf26a5c Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Tue, 24 Jul 2018 20:32:55 -0700 Subject: [PATCH 033/185] Attempt to work around a weird issue with the docs build --- ci/rtd-requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ci/rtd-requirements.txt b/ci/rtd-requirements.txt index 67898ba..ea59c6b 100644 --- a/ci/rtd-requirements.txt +++ b/ci/rtd-requirements.txt @@ -2,3 +2,6 @@ sphinx >= 1.6.1 sphinx_rtd_theme sphinxcontrib-trio +# Workaround for this weird issue: +# https://travis-ci.org/python-trio/pytest-trio/jobs/407495415 +attrs >= 17.4.0 From eb42b8a9ab964d6299b8f0a00982524c282c4007 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Tue, 24 Jul 2018 20:52:04 -0700 Subject: [PATCH 034/185] In Trio mode, auto-convert async fixtures to Trio fixtures The main benefit of this is that it lets us catch more cases where these fixtures are accidentally misused. Closes gh-18 Closes gh-51 --- pytest_trio/_tests/helpers.py | 15 ++++++ pytest_trio/_tests/test_fixture_mistakes.py | 59 +++++++++++++++++++-- pytest_trio/_tests/test_trio_mode.py | 16 ++---- pytest_trio/enable_trio_mode.py | 8 ++- pytest_trio/plugin.py | 23 +++++--- 5 files changed, 97 insertions(+), 24 deletions(-) create mode 100644 pytest_trio/_tests/helpers.py diff --git a/pytest_trio/_tests/helpers.py b/pytest_trio/_tests/helpers.py new file mode 100644 index 0000000..5ae5f9e --- /dev/null +++ b/pytest_trio/_tests/helpers.py @@ -0,0 +1,15 @@ +import pytest + + +def enable_trio_mode_via_pytest_ini(testdir): + testdir.makefile(".ini", pytest="[pytest]\ntrio_mode = true\n") + + +def enable_trio_mode_via_conftest_py(testdir): + testdir.makeconftest("from pytest_trio.enable_trio_mode import *") + + +enable_trio_mode = pytest.mark.parametrize( + "enable_trio_mode", + [enable_trio_mode_via_pytest_ini, enable_trio_mode_via_conftest_py] +) diff --git a/pytest_trio/_tests/test_fixture_mistakes.py b/pytest_trio/_tests/test_fixture_mistakes.py index 2e775f2..5c6873c 100644 --- a/pytest_trio/_tests/test_fixture_mistakes.py +++ b/pytest_trio/_tests/test_fixture_mistakes.py @@ -1,6 +1,8 @@ import pytest from pytest_trio import trio_fixture +from .helpers import enable_trio_mode + def test_trio_fixture_with_non_trio_test(testdir): testdir.makepyfile( @@ -34,11 +36,11 @@ def test_sync_indirect(indirect_trio_time): result.assert_outcomes(passed=1, error=2) result.stdout.fnmatch_lines( - ["*Trio fixtures can only be used by Trio tests*"] + ["*: Trio fixtures can only be used by Trio tests*"] ) -def test_trio_fixture_with_wrong_scope(testdir): +def test_trio_fixture_with_wrong_scope_without_trio_mode(testdir): # There's a trick here: when you have a non-function-scope fixture, it's # not instantiated for any particular function (obviously). So... when our # pytest_fixture_setup hook tries to check for marks, it can't normally @@ -49,7 +51,6 @@ def test_trio_fixture_with_wrong_scope(testdir): testdir.makepyfile( """ import pytest - import pytest_trio @pytest.fixture(scope="class") async def async_class_fixture(): @@ -65,4 +66,54 @@ async def test_foo(self, async_class_fixture): result = testdir.runpytest() result.assert_outcomes(error=1) - result.stdout.fnmatch_lines(["*must be function-scope*"]) + result.stdout.fnmatch_lines(["*: Trio fixtures must be function-scope*"]) + + +@enable_trio_mode +def test_trio_fixture_with_wrong_scope_in_trio_mode(testdir, enable_trio_mode): + enable_trio_mode(testdir) + + testdir.makepyfile( + """ + import pytest + + @pytest.fixture(scope="session") + async def async_session_fixture(): + pass + + + async def test_whatever(async_session_fixture): + pass + """ + ) + + result = testdir.runpytest() + + result.assert_outcomes(error=1) + result.stdout.fnmatch_lines(["*: Trio fixtures must be function-scope*"]) + + +@enable_trio_mode +def test_async_fixture_with_sync_test_in_trio_mode(testdir, enable_trio_mode): + enable_trio_mode(testdir) + + testdir.makepyfile( + """ + import pytest + + @pytest.fixture + async def async_fixture(): + pass + + + def test_whatever(async_fixture): + pass + """ + ) + + result = testdir.runpytest() + + result.assert_outcomes(error=1) + result.stdout.fnmatch_lines( + ["*: Trio fixtures can only be used by Trio tests*"] + ) diff --git a/pytest_trio/_tests/test_trio_mode.py b/pytest_trio/_tests/test_trio_mode.py index 764f5da..efc66a8 100644 --- a/pytest_trio/_tests/test_trio_mode.py +++ b/pytest_trio/_tests/test_trio_mode.py @@ -1,5 +1,7 @@ import pytest +from .helpers import enable_trio_mode + test_text = """ import pytest import trio @@ -26,19 +28,11 @@ async def test_hypothesis_fail(b): """ -def test_trio_mode_pytest_ini(testdir): - testdir.makepyfile(test_text) - - testdir.makefile(".ini", pytest="[pytest]\ntrio_mode = true\n") - - result = testdir.runpytest() - result.assert_outcomes(passed=2, failed=2) +@enable_trio_mode +def test_trio_mode(testdir, enable_trio_mode): + enable_trio_mode(testdir) - -def test_trio_mode_conftest(testdir): testdir.makepyfile(test_text) - testdir.makeconftest("from pytest_trio.enable_trio_mode import *") - result = testdir.runpytest() result.assert_outcomes(passed=2, failed=2) diff --git a/pytest_trio/enable_trio_mode.py b/pytest_trio/enable_trio_mode.py index 6f05933..0c13614 100644 --- a/pytest_trio/enable_trio_mode.py +++ b/pytest_trio/enable_trio_mode.py @@ -1,7 +1,11 @@ -__all__ = ["pytest_collection_modifyitems"] +__all__ = ["pytest_collection_modifyitems", "pytest_fixture_setup"] -from .plugin import automark +from .plugin import automark, handle_fixture def pytest_collection_modifyitems(items): automark(items) + + +def pytest_fixture_setup(fixturedef, request): + return handle_fixture(fixturedef, request, force_trio_mode=True) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 079b930..4d28c42 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -214,22 +214,26 @@ def trio_fixture(func): return pytest.fixture(func) -def _is_trio_fixture(func, is_trio_test, deps): +def _is_trio_fixture(func, coerce_async, deps): if getattr(func, "_force_trio_fixture", False): return True - if is_trio_test: - if iscoroutinefunction(func) or isasyncgenfunction(func): - return True + if (coerce_async and + (iscoroutinefunction(func) or isasyncgenfunction(func))): + return True if any(isinstance(dep, TrioFixture) for dep in deps.values()): return True return False -@pytest.hookimpl -def pytest_fixture_setup(fixturedef, request): +def handle_fixture(fixturedef, request, force_trio_mode): is_trio_test = (request.node.get_closest_marker("trio") is not None) + if force_trio_mode: + is_trio_mode = True + else: + is_trio_mode = request.node.config.getini("trio_mode") + coerce_async = (is_trio_test or is_trio_mode) deps = {dep: request.getfixturevalue(dep) for dep in fixturedef.argnames} - if _is_trio_fixture(fixturedef.func, is_trio_test, deps): + if _is_trio_fixture(fixturedef.func, coerce_async, deps): if request.scope != "function": raise RuntimeError("Trio fixtures must be function-scope") if not is_trio_test: @@ -239,6 +243,11 @@ def pytest_fixture_setup(fixturedef, request): return fixture +@pytest.hookimpl +def pytest_fixture_setup(fixturedef, request): + return handle_fixture(fixturedef, request, force_trio_mode=False) + + ################################################################ # Trio mode ################################################################ From fe65b49af119f7eed89ded1ad4bb538d3aa14c41 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Wed, 15 Aug 2018 00:14:47 -0700 Subject: [PATCH 035/185] Totally redo fixture/test setup/teardown, yet again See https://github.com/python-trio/pytest-trio/issues/55 for discussion. Main points: * Fixture setup/teardown is now performed concurrently whenever possible. * Each fixture is now run in an isolated task, and it's now safe to use nurseries inside fixtures (fixes gh-55). * New magical fixture 'fixture_nursery' which gives a nursery that's cancelled immediately after *the requesting fixture finishes teardown*. If requested directly by the test itself, gives a nursery which is cancelled after the test finishes, before other fixtures are torn down. --- .../_tests/test_async_yield_fixture.py | 20 +- pytest_trio/_tests/test_basic.py | 2 +- ...ery_fixture.py => test_fixture_nursery.py} | 4 +- pytest_trio/_tests/test_sync_fixture.py | 20 +- pytest_trio/plugin.py | 385 ++++++++++++------ 5 files changed, 284 insertions(+), 147 deletions(-) rename pytest_trio/_tests/{test_nursery_fixture.py => test_fixture_nursery.py} (78%) diff --git a/pytest_trio/_tests/test_async_yield_fixture.py b/pytest_trio/_tests/test_async_yield_fixture.py index b46dd06..4810456 100644 --- a/pytest_trio/_tests/test_async_yield_fixture.py +++ b/pytest_trio/_tests/test_async_yield_fixture.py @@ -317,39 +317,43 @@ def test_async_yield_fixture_crashed_teardown_allow_other_teardowns( import trio from async_generator import async_generator, yield_ - events = [] + setup_events = set() + teardown_events = set() @pytest.fixture @async_generator async def good_fixture(): async with trio.open_nursery() as nursery: - events.append('good_fixture setup') + setup_events.add('good_fixture setup') await yield_(None) - events.append('good_fixture teardown') + teardown_events.add('good_fixture teardown') @pytest.fixture @async_generator async def bad_fixture(): async with trio.open_nursery() as nursery: - events.append('bad_fixture setup') + setup_events.add('bad_fixture setup') await yield_(None) - events.append('bad_fixture teardown') + teardown_events.add('bad_fixture teardown') raise RuntimeError('Crash during fixture teardown') def test_before(): - assert not events + assert not setup_events + assert not teardown_events @pytest.mark.trio async def test_actual_test(bad_fixture, good_fixture): pass def test_after(): - assert events == [ + assert setup_events == { 'good_fixture setup', 'bad_fixture setup', + } + assert teardown_events == { 'bad_fixture teardown', 'good_fixture teardown', - ] + } """ ) ) diff --git a/pytest_trio/_tests/test_basic.py b/pytest_trio/_tests/test_basic.py index 6ae233f..d442ee6 100644 --- a/pytest_trio/_tests/test_basic.py +++ b/pytest_trio/_tests/test_basic.py @@ -21,7 +21,7 @@ def test_check_async_test_called(): """ ) - result = testdir.runpytest() + result = testdir.runpytest("-s") result.assert_outcomes(passed=2) diff --git a/pytest_trio/_tests/test_nursery_fixture.py b/pytest_trio/_tests/test_fixture_nursery.py similarity index 78% rename from pytest_trio/_tests/test_nursery_fixture.py rename to pytest_trio/_tests/test_fixture_nursery.py index 2a006d4..7cbb409 100644 --- a/pytest_trio/_tests/test_nursery_fixture.py +++ b/pytest_trio/_tests/test_fixture_nursery.py @@ -9,8 +9,8 @@ async def handle_client(stream): @pytest.fixture -async def server(test_nursery): - listeners = await test_nursery.start(trio.serve_tcp, handle_client, 0) +async def server(fixture_nursery): + listeners = await fixture_nursery.start(trio.serve_tcp, handle_client, 0) return listeners[0] diff --git a/pytest_trio/_tests/test_sync_fixture.py b/pytest_trio/_tests/test_sync_fixture.py index 877da28..7a4cb0f 100644 --- a/pytest_trio/_tests/test_sync_fixture.py +++ b/pytest_trio/_tests/test_sync_fixture.py @@ -96,7 +96,8 @@ def test_sync_yield_fixture_crashed_teardown_allow_other_teardowns(testdir): import pytest import trio - events = [] + setup_events = set() + teardown_events = set() @pytest.fixture async def force_async_fixture(): @@ -104,31 +105,34 @@ async def force_async_fixture(): @pytest.fixture def good_fixture(force_async_fixture): - events.append('good_fixture setup') + setup_events.add('good_fixture setup') yield - events.append('good_fixture teardown') + teardown_events.add('good_fixture teardown') @pytest.fixture def bad_fixture(force_async_fixture): - events.append('bad_fixture setup') + setup_events.add('bad_fixture setup') yield - events.append('bad_fixture teardown') + teardown_events.add('bad_fixture teardown') raise RuntimeError('Crash during fixture teardown') def test_before(): - assert not events + assert not setup_events + assert not teardown_events @pytest.mark.trio async def test_actual_test(bad_fixture, good_fixture): pass def test_after(): - assert events == [ + assert setup_events == { 'good_fixture setup', 'bad_fixture setup', + } + assert teardown_events == { 'bad_fixture teardown', 'good_fixture teardown', - ] + } """ ) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 4d28c42..98f41c9 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -48,9 +48,233 @@ def pytest_exception_interact(node, call, report): ################################################################ -# Core support for running tests and constructing fixtures +# Core support for trio fixtures and trio tests ################################################################ +# This is more complicated than you might expect. + +# The first complication is that all of pytest's machinery for setting up, +# running a test, and then tearing it down again is synchronous. But we want +# to have async setup, async tests, and async teardown. +# +# Our trick: from pytest's point of view, trio fixtures return an unevaluated +# placeholder value, a TrioFixture object. This contains all the information +# needed to do the actual setup/teardown, but doesn't actually perform these +# operations. +# +# Then, pytest runs what it thinks of as "the test", we enter trio, and use +# our own logic to setup the trio fixtures, run the actual test, and then tear +# down the trio fixtures. This works pretty well, though it has some +# limitations: +# - trio fixtures have to be test-scoped +# - normally pytest considers a fixture crash to be an ERROR, but when a trio +# fixture crashes, it gets classified as a FAIL. + +# The other major complication is that we really want to allow trio fixtures +# to yield inside a nursery. (See gh-55 for more discussion.) And then while +# the fixture function is suspended, a task inside that nursery might crash. +# +# Why is this a problem? Two reasons. First, a technical one: Trio's cancel +# scope machinery assumes that it can inject a Cancelled exception into any +# code inside the cancel scope, and that exception will eventually make its +# way back to the 'with' block. +# +# A fixture that yields inside a nursery violates this rule: the cancel scope +# remains "active" from when the fixture yields until when it's reentered, but +# if a Cancelled exception is raised during this time, then it *won't* go into +# the fixture. (And we can't throw it in there either, because that's just not +# how pytest fixtures work. Whoops.) +# +# And second, our setup/test/teardown process needs to account for the +# possibility that any fixture's background task might crash at any moment, +# and do something sensible with it. +# +# You should think of fixtures as a dependency graph: each fixtures *uses* +# zero or more other fixtures, and is *used by* zero or more other fixtures. A +# fixture should be setup before any of the fixtures it's used by are setup, +# and it should be torn down as soon as all of the fixtures that use it are +# torn down. And at root of this dependency graph, we have the test itself, +# which is just like a fixture except that instead of having a separate setup +# and teardown phase, it runs straight through. +# +# To implement this, we isolate each fixture into its own task: this makes +# sure that crashes in one can't trigger implicit cancellation in another. +# Then we use trio.Event objects to implement the ordering described above. +# +# If a fixture crashes, whether during setup, teardown, or in a background +# task at any other point, then we mark the whole test run as "crashed". When +# a run is "crashed", two things happen: (1) if any fixtures or the test +# itself haven't started yet, then we don't start them. (2) if the test is +# running, we cancel it. That's all. In particular, if a fixture has a +# background crash, we don't propagate that to any other fixtures, we still +# follow the normal teardown sequence, and so on – but since the test is +# cancelled, the teardown sequence should start immediately. + +class TrioTestContext: + def __init__(self): + self.crashed = False + self.test_cancel_scope = None + self.error_list = [] + + def crash(self, exc): + if exc is not None: + self.error_list.append(exc) + self.crashed = True + if self.test_cancel_scope is not None: + self.test_cancel_scope.cancel() + + +class TrioFixture: + """ + Represent a fixture that need to be run in a trio context to be resolved. + + The name is actually a misnomer, because we use it to represent the actual + test itself as well, since the test is basically just a fixture with no + dependents and no teardown. + """ + + def __init__(self, func, pytest_kwargs, is_test=False): + self._func = func + self._pytest_kwargs = pytest_kwargs + self._is_test = is_test + self._teardown_done = trio.Event() + + # These attrs are all accessed from other objects: + # Downstream users read this value. + self.fixture_value = None + # This event notifies downstream users that we're done setting up. + # Invariant: if this is set, then either fixture_value is usable *or* + # ctx.crashed is True. + self.setup_done = trio.Event() + # Downstream users *modify* this value, by adding their _teardown_done + # events to it, so we know who we need to wait for before tearing + # down. + self.user_done_events = set() + + def register_and_collect_dependencies(self): + # Returns the set of all TrioFixtures that this fixture depends on, + # directly or indirectly, and sets up all their user_done_events. + deps = set() + deps.add(self) + for value in self._pytest_kwargs.values(): + if isinstance(value, TrioFixture): + value.user_done_events.add(self._teardown_done) + deps.update(value.register_and_collect_dependencies()) + return deps + + @asynccontextmanager + @async_generator + async def _fixture_manager(self, ctx): + __tracebackhide__ = True + try: + async with trio.open_nursery() as fixture_nursery: + try: + await yield_(fixture_nursery) + finally: + fixture_nursery.cancel_scope.cancel() + except BaseException as exc: + print(repr(exc)) + ctx.crash(exc) + finally: + self.setup_done.set() + self._teardown_done.set() + + async def run(self, ctx): + __tracebackhide__ = True + + # This 'with' block handles the fixture_nursery lifetime, the + # teardone_done event, and crashing the context if there's an + # unhandled exception. + async with self._fixture_manager(ctx) as fixture_nursery: + # Resolve our kwargs + resolved_kwargs = {} + for name, value in self._pytest_kwargs.items(): + if isinstance(value, TrioFixture): + await value.setup_done.wait() + if value.fixture_value is FIXTURE_NURSERY_PLACEHOLDER: + resolved_kwargs[name] = fixture_nursery + else: + resolved_kwargs[name] = value.fixture_value + else: + resolved_kwargs[name] = value + + # If something's already crashed before we're ready to start, then + # there's no point in even setting up. + if ctx.crashed: + return + + # Run actual fixture setup step + if self._is_test: + # Tests are exactly like fixtures, except that they (1) have + # to be regular async functions, (2) if there's a crash, we + # should cancel them. + assert not self.user_done_events + func_value = None + with trio.open_cancel_scope() as cancel_scope: + ctx.test_cancel_scope = cancel_scope + assert not ctx.crashed + await self._func(**resolved_kwargs) + else: + func_value = self._func(**resolved_kwargs) + if isinstance(func_value, Coroutine): + self.fixture_value = await func_value + elif isasyncgen(func_value): + self.fixture_value = await func_value.asend(None) + elif isinstance(func_value, Generator): + self.fixture_value = func_value.send(None) + else: + # Regular synchronous function + self.fixture_value = func_value + + # Notify our users that self.fixture_value is ready + self.setup_done.set() + + # Wait for users to be finished + # + # At this point we're in a very strange state: if the fixture + # yielded inside a nursery or cancel scope, then we are still + # "inside" that scope even though its with block is not on the + # stack. In particular this means that if they get cancelled, then + # our waiting might get a Cancelled error, that we cannot really + # deal with – it should get thrown back into the fixture + # generator, but pytest fixture generators don't work that way: + # https://github.com/python-trio/pytest-trio/issues/55 + # And besides, we can't start tearing down until all our users + # have finished. + # + # So if we get an exception here, we crash the context (which + # cancels the test and starts the cleanup process), save any + # exception that *isn't* Cancelled (because if its Cancelled then + # we can't route it to the right place, and anyway the teardown + # code will get it again if it matters), and then use a shield to + # keep waiting for the teardown to finish without having to worry + # about cancellation. + try: + for event in self.user_done_events: + await event.wait() + except BaseException as exc: + assert isinstance(exc, trio.Cancelled) + ctx.crash(None) + with trio.open_cancel_scope(shield=True): + for event in self.user_done_events: + await event.wait() + + # Do our teardown + if isasyncgen(func_value): + try: + await func_value.asend(None) + except StopAsyncIteration: + pass + else: + raise RuntimeError("too many yields in fixture") + elif isinstance(func_value, Generator): + try: + func_value.send(None) + except StopIteration: + pass + else: + raise RuntimeError("too many yields in fixture") + def _trio_test_runner_factory(item, testfunc=None): testfunc = testfunc or item.function @@ -66,129 +290,32 @@ def _trio_test_runner_factory(item, testfunc=None): ) @trio_test - async def _bootstrap_fixture_and_run_test(**kwargs): + async def _bootstrap_fixtures_and_run_test(**kwargs): __tracebackhide__ = True - user_exc = None - # Open the nursery exposed as fixture - async with trio.open_nursery() as nursery: - item._trio_nursery = nursery - try: - async with _setup_async_fixtures_in(kwargs) as resolved_kwargs: - try: - await testfunc(**resolved_kwargs) - except BaseException as exc: - # Regular pytest fixture don't have access to the test - # exception in there teardown, we mimic this behavior - # here. - user_exc = exc - except BaseException as exc: - # If we are here, the exception comes from the fixtures setup - # or teardown - if user_exc: - raise exc from user_exc - else: - raise exc - finally: - # No matter what the nursery fixture should be closed when - # test is over - nursery.cancel_scope.cancel() - - # Finally re-raise or original exception coming from the test if - # needed - if user_exc: - raise user_exc - - _bootstrap_fixture_and_run_test._trio_test_runner_wrapped = True - return _bootstrap_fixture_and_run_test + ctx = TrioTestContext() + test = TrioFixture(testfunc, kwargs, is_test=True) -@asynccontextmanager -@async_generator -async def _setup_async_fixtures_in(deps): - __tracebackhide__ = True - - need_resolved_deps_stack = [ - (k, v) for k, v in deps.items() if isinstance(v, TrioFixture) - ] - if not ORDERED_DICTS: - # Make the fixture resolution order determinist - need_resolved_deps_stack = sorted(need_resolved_deps_stack) - - if not need_resolved_deps_stack: - await yield_(deps) - return - - @asynccontextmanager - @async_generator - async def _recursive_setup(deps_stack): - __tracebackhide__ = True - name, dep = deps_stack.pop() - async with dep.setup() as resolved: - if not deps_stack: - await yield_([(name, resolved)]) - else: - async with _recursive_setup(deps_stack - ) as remains_deps_stack_resolved: - await yield_( - remains_deps_stack_resolved + [(name, resolved)] - ) - - async with _recursive_setup(need_resolved_deps_stack - ) as resolved_deps_stack: - await yield_({**deps, **dict(resolved_deps_stack)}) - + async with trio.open_nursery() as nursery: + for fixture in test.register_and_collect_dependencies(): + nursery.start_soon(fixture.run, ctx) -class TrioFixture: - """ - Represent a fixture that need to be run in a trio context to be resolved. - """ + if ctx.error_list: + raise trio.MultiError(ctx.error_list) - def __init__(self, fixturedef, deps={}): - self.fixturedef = fixturedef - self.deps = deps - self.setup_done = False - self.result = None + _bootstrap_fixtures_and_run_test._trio_test_runner_wrapped = True + return _bootstrap_fixtures_and_run_test - @asynccontextmanager - @async_generator - async def setup(self): - __tracebackhide__ = True - if self.setup_done: - await yield_(self.result) - else: - async with _setup_async_fixtures_in(self.deps) as resolved_deps: - retval = self.fixturedef.func(**resolved_deps) - if isinstance(retval, Coroutine): - self.result = await retval - elif isasyncgen(retval): - self.result = await retval.asend(None) - elif isinstance(retval, Generator): - self.result = retval.send(None) - else: - # Regular synchronous function - self.result = retval - try: - await yield_(self.result) - finally: - if isasyncgen(retval): - try: - await retval.asend(None) - except StopAsyncIteration: - pass - else: - raise RuntimeError("too many yields in fixture") - elif isinstance(retval, Generator): - try: - retval.send(None) - except StopIteration: - pass - else: - raise RuntimeError("too many yields in fixture") +################################################################ +# Hooking up the test/fixture machinery to pytest +################################################################ @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(item): + print(item) + print(item.get_closest_marker("trio")) if item.get_closest_marker("trio") is not None: if hasattr(item.obj, 'hypothesis'): # If it's a Hypothesis test, we go in a layer. @@ -214,13 +341,13 @@ def trio_fixture(func): return pytest.fixture(func) -def _is_trio_fixture(func, coerce_async, deps): +def _is_trio_fixture(func, coerce_async, kwargs): if getattr(func, "_force_trio_fixture", False): return True if (coerce_async and (iscoroutinefunction(func) or isasyncgenfunction(func))): return True - if any(isinstance(dep, TrioFixture) for dep in deps.values()): + if any(isinstance(value, TrioFixture) for value in kwargs.values()): return True return False @@ -232,18 +359,17 @@ def handle_fixture(fixturedef, request, force_trio_mode): else: is_trio_mode = request.node.config.getini("trio_mode") coerce_async = (is_trio_test or is_trio_mode) - deps = {dep: request.getfixturevalue(dep) for dep in fixturedef.argnames} - if _is_trio_fixture(fixturedef.func, coerce_async, deps): + kwargs = {name: request.getfixturevalue(name) for name in fixturedef.argnames} + if _is_trio_fixture(fixturedef.func, coerce_async, kwargs): if request.scope != "function": raise RuntimeError("Trio fixtures must be function-scope") if not is_trio_test: raise RuntimeError("Trio fixtures can only be used by Trio tests") - fixture = TrioFixture(fixturedef, deps) + fixture = TrioFixture(fixturedef.func, kwargs) fixturedef.cached_result = (fixture, request.param_index, None) return fixture -@pytest.hookimpl def pytest_fixture_setup(fixturedef, request): return handle_fixture(fixturedef, request, force_trio_mode=False) @@ -273,6 +399,15 @@ def pytest_collection_modifyitems(config, items): ################################################################ +class FIXTURE_NURSERY_PLACEHOLDER: + pass + + +@trio_fixture +def fixture_nursery(): + return FIXTURE_NURSERY_PLACEHOLDER + + @pytest.fixture def mock_clock(): return MockClock() @@ -284,16 +419,10 @@ def autojump_clock(): @trio_fixture -def test_nursery(request): - return request.node._trio_nursery - - -@trio_fixture -def nursery(test_nursery): - import warnings +def nursery(request): warnings.warn( FutureWarning( - "The 'nursery' fixture has been renamed to 'test_nursery'" + "The 'nursery' fixture has been deprecated; use fixture_nursery instead" ) ) - return test_nursery + return FIXTURE_NURSERY_PLACEHOLDER From 4a477a524910283368d4b63c82924a65843f180e Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Wed, 15 Aug 2018 00:21:09 -0700 Subject: [PATCH 036/185] Remove stray prints --- pytest_trio/plugin.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 98f41c9..8760579 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -173,7 +173,6 @@ async def _fixture_manager(self, ctx): finally: fixture_nursery.cancel_scope.cancel() except BaseException as exc: - print(repr(exc)) ctx.crash(exc) finally: self.setup_done.set() @@ -314,8 +313,6 @@ async def _bootstrap_fixtures_and_run_test(**kwargs): @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(item): - print(item) - print(item.get_closest_marker("trio")) if item.get_closest_marker("trio") is not None: if hasattr(item.obj, 'hypothesis'): # If it's a Hypothesis test, we go in a layer. From 4f8a019cdffc9624f01c8c4f499b67a03ad8f713 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Wed, 15 Aug 2018 01:06:13 -0700 Subject: [PATCH 037/185] Avoid using broken pytests when testing pytest-trio --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index ffdc000..b2be0f9 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,3 @@ -pytest +pytest !=3.7.0, !=3.7.1 # https://github.com/python-trio/pytest-trio/pull/50#issuecomment-413124393 pytest-cov hypothesis>=3.64 From 4e31ce988c7f4b772769e16379a83fe319a1f060 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Wed, 15 Aug 2018 01:11:22 -0700 Subject: [PATCH 038/185] yapf --- pytest_trio/plugin.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 8760579..e5bba95 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -110,6 +110,7 @@ def pytest_exception_interact(node, call, report): # follow the normal teardown sequence, and so on – but since the test is # cancelled, the teardown sequence should start immediately. + class TrioTestContext: def __init__(self): self.crashed = False @@ -356,7 +357,10 @@ def handle_fixture(fixturedef, request, force_trio_mode): else: is_trio_mode = request.node.config.getini("trio_mode") coerce_async = (is_trio_test or is_trio_mode) - kwargs = {name: request.getfixturevalue(name) for name in fixturedef.argnames} + kwargs = { + name: request.getfixturevalue(name) + for name in fixturedef.argnames + } if _is_trio_fixture(fixturedef.func, coerce_async, kwargs): if request.scope != "function": raise RuntimeError("Trio fixtures must be function-scope") From 25e9e19b9ca099e8dc301586b7ef45194559d98b Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Wed, 15 Aug 2018 02:02:52 -0700 Subject: [PATCH 039/185] Revert back to 'nursery' as the name of the magic nursery fixture At this point this: @pytest.fixture async fix(nursery): ... is just a shorthand for this (they should be exactly equivalent): @pytest.fixture async fix(): async with trio.open_nursery() as nursery: try: ... finally: nursery.cancel_scope.cancel() And the latter is the obvious thing you would write by hand, there's really no other way to do it. And since it no longer has special subtle semantics, I'm no longer bothered by using the short name 'nursery'. Plus the longer I looked at 'fixture_nursery' the more annoying the name seemed. Plus I made that last minute change so you can use the magic nursery inside tests too, not just fixtures, so the name didn't even make much sense. --- pytest_trio/_tests/test_fixture_nursery.py | 4 ++-- pytest_trio/plugin.py | 28 +++++++--------------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/pytest_trio/_tests/test_fixture_nursery.py b/pytest_trio/_tests/test_fixture_nursery.py index 7cbb409..7719fa1 100644 --- a/pytest_trio/_tests/test_fixture_nursery.py +++ b/pytest_trio/_tests/test_fixture_nursery.py @@ -9,8 +9,8 @@ async def handle_client(stream): @pytest.fixture -async def server(fixture_nursery): - listeners = await fixture_nursery.start(trio.serve_tcp, handle_client, 0) +async def server(nursery): + listeners = await nursery.start(trio.serve_tcp, handle_client, 0) return listeners[0] diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index e5bba95..46f4d6a 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -168,11 +168,11 @@ def register_and_collect_dependencies(self): async def _fixture_manager(self, ctx): __tracebackhide__ = True try: - async with trio.open_nursery() as fixture_nursery: + async with trio.open_nursery() as nursery_fixture: try: - await yield_(fixture_nursery) + await yield_(nursery_fixture) finally: - fixture_nursery.cancel_scope.cancel() + nursery_fixture.cancel_scope.cancel() except BaseException as exc: ctx.crash(exc) finally: @@ -182,17 +182,17 @@ async def _fixture_manager(self, ctx): async def run(self, ctx): __tracebackhide__ = True - # This 'with' block handles the fixture_nursery lifetime, the + # This 'with' block handles the nursery fixture lifetime, the # teardone_done event, and crashing the context if there's an # unhandled exception. - async with self._fixture_manager(ctx) as fixture_nursery: + async with self._fixture_manager(ctx) as nursery_fixture: # Resolve our kwargs resolved_kwargs = {} for name, value in self._pytest_kwargs.items(): if isinstance(value, TrioFixture): await value.setup_done.wait() - if value.fixture_value is FIXTURE_NURSERY_PLACEHOLDER: - resolved_kwargs[name] = fixture_nursery + if value.fixture_value is NURSERY_FIXTURE_PLACEHOLDER: + resolved_kwargs[name] = nursery_fixture else: resolved_kwargs[name] = value.fixture_value else: @@ -400,15 +400,10 @@ def pytest_collection_modifyitems(config, items): ################################################################ -class FIXTURE_NURSERY_PLACEHOLDER: +class NURSERY_FIXTURE_PLACEHOLDER: pass -@trio_fixture -def fixture_nursery(): - return FIXTURE_NURSERY_PLACEHOLDER - - @pytest.fixture def mock_clock(): return MockClock() @@ -421,9 +416,4 @@ def autojump_clock(): @trio_fixture def nursery(request): - warnings.warn( - FutureWarning( - "The 'nursery' fixture has been deprecated; use fixture_nursery instead" - ) - ) - return FIXTURE_NURSERY_PLACEHOLDER + return NURSERY_FIXTURE_PLACEHOLDER From 0154411c67d0f8e24adb3aaad07d64956335f438 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Thu, 16 Aug 2018 23:22:16 -0700 Subject: [PATCH 040/185] Add more substantial tests for fixture ordering subtleties --- pytest_trio/_tests/test_fixture_ordering.py | 284 ++++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 pytest_trio/_tests/test_fixture_ordering.py diff --git a/pytest_trio/_tests/test_fixture_ordering.py b/pytest_trio/_tests/test_fixture_ordering.py new file mode 100644 index 0000000..788d10d --- /dev/null +++ b/pytest_trio/_tests/test_fixture_ordering.py @@ -0,0 +1,284 @@ +import pytest + +# Tests that: +# - leaf_fix gets set up first and torn down last +# - the two fix_concurrent_{1,2} fixtures run their setup/teardown code +# at the same time -- their execution can be interleaved. +def test_fixture_basic_ordering(testdir): + testdir.makepyfile( + """ + import pytest + from pytest_trio import trio_fixture + from trio.testing import Sequencer + from async_generator import async_generator, yield_ + + setup_events = [] + teardown_events = [] + + @trio_fixture + def seq(): + return Sequencer() + + @pytest.fixture + @async_generator + async def leaf_fix(): + setup_events.append("leaf_fix setup") + await yield_() + teardown_events.append("leaf_fix teardown") + + assert teardown_events == [ + "fix_concurrent_1 teardown 1", + "fix_concurrent_2 teardown 1", + "fix_concurrent_1 teardown 2", + "fix_concurrent_2 teardown 2", + "leaf_fix teardown", + ] + + @pytest.fixture + @async_generator + async def fix_concurrent_1(leaf_fix, seq): + async with seq(0): + setup_events.append("fix_concurrent_1 setup 1") + async with seq(2): + setup_events.append("fix_concurrent_1 setup 2") + await yield_() + async with seq(4): + teardown_events.append("fix_concurrent_1 teardown 1") + async with seq(6): + teardown_events.append("fix_concurrent_1 teardown 2") + + @pytest.fixture + @async_generator + async def fix_concurrent_2(leaf_fix, seq): + async with seq(1): + setup_events.append("fix_concurrent_2 setup 1") + async with seq(3): + setup_events.append("fix_concurrent_2 setup 2") + await yield_() + async with seq(5): + teardown_events.append("fix_concurrent_2 teardown 1") + async with seq(7): + teardown_events.append("fix_concurrent_2 teardown 2") + + @pytest.mark.trio + async def test_root(fix_concurrent_1, fix_concurrent_2): + assert setup_events == [ + "leaf_fix setup", + "fix_concurrent_1 setup 1", + "fix_concurrent_2 setup 1", + "fix_concurrent_1 setup 2", + "fix_concurrent_2 setup 2", + ] + assert teardown_events == [] + + """ + ) + + result = testdir.runpytest() + result.assert_outcomes(passed=1) + + +def test_nursery_fixture_teardown_ordering(testdir): + testdir.makepyfile( + """ + import pytest + from pytest_trio import trio_fixture + import trio + from trio.testing import wait_all_tasks_blocked + + events = [] + + async def record_cancel(msg): + try: + await trio.sleep_forever() + finally: + events.append(msg) + + @pytest.fixture + def fix0(): + yield + assert events == [ + "test", + "test cancel", + "fix2 teardown", + "fix2 cancel", + "fix1 teardown", + "fix1 cancel", + ] + + @trio_fixture + def fix1(nursery): + nursery.start_soon(record_cancel, "fix1 cancel") + yield + events.append("fix1 teardown") + + @trio_fixture + def fix2(fix1, nursery): + nursery.start_soon(record_cancel, "fix2 cancel") + yield + events.append("fix2 teardown") + + @pytest.mark.trio + async def test_root(fix2, nursery): + nursery.start_soon(record_cancel, "test cancel") + await wait_all_tasks_blocked() + events.append("test") + """ + ) + + result = testdir.runpytest() + result.assert_outcomes(passed=1) + + +def test_error_collection(testdir): + # We want to make sure that pytest ultimately reports all the different + # exceptions. We call .upper() on all the exceptions so that we have + # tokens to look for in the output corresponding to each exception, where + # those tokens don't appear at all the source (so we can't get a false + # positive due to pytest printing out the source file). + + # We sleep at the beginning of all the fixtures b/c currently if any + # fixture crashes, we skip setting up unrelated fixtures whose setup + # hasn't even started yet. Maybe we shouldn't? But for now the sleeps make + # sure that all the fixtures have started before any of them start + # crashing. + testdir.makepyfile( + """ + import pytest + from pytest_trio import trio_fixture + import trio + from async_generator import async_generator, yield_ + + test_started = False + + @trio_fixture + async def crash_nongen(): + await trio.sleep(2) + raise RuntimeError("crash_nongen".upper()) + + @trio_fixture + @async_generator + async def crash_early_agen(): + await trio.sleep(2) + raise RuntimeError("crash_early_agen".upper()) + await yield_() + + @trio_fixture + @async_generator + async def crash_late_agen(): + await yield_() + raise RuntimeError("crash_late_agen".upper()) + + async def crash(when, token): + with trio.open_cancel_scope(shield=True): + await trio.sleep(when) + raise RuntimeError(token.upper()) + + @trio_fixture + def crash_background(nursery): + nursery.start_soon(crash, 1, "crash_background_early") + nursery.start_soon(crash, 3, "crash_background_late") + + @pytest.mark.trio + async def test_all_the_crashes( + autojump_clock, + crash_nongen, crash_early_agen, crash_late_agen, crash_background, + ): + global test_started + test_started = True + + def test_followup(): + assert not test_started + + """ + ) + + result = testdir.runpytest() + result.assert_outcomes(passed=1, failed=1) + result.stdout.fnmatch_lines_random([ + "*CRASH_NONGEN*", + "*CRASH_EARLY_AGEN*", + "*CRASH_LATE_AGEN*", + "*CRASH_BACKGROUND_EARLY*", + "*CRASH_BACKGROUND_LATE*", + ]) + + +@pytest.mark.parametrize("bgmode", ["nursery fixture", "manual nursery"]) +def test_background_crash_cancellation_propagation(bgmode, testdir): + crashyfix_using_nursery_fixture = """ + @trio_fixture + def crashyfix(nursery): + nursery.start_soon(crashy) + yield + # We should be cancelled here + teardown_deadlines["crashyfix"] = trio.current_effective_deadline() + """ + + crashyfix_using_manual_nursery = """ + @trio_fixture + @async_generator + async def crashyfix(): + async with trio.open_nursery() as nursery: + nursery.start_soon(crashy) + await yield_() + # We should be cancelled here + teardown_deadlines["crashyfix"] = trio.current_effective_deadline() + """ + + if bgmode == "nursery fixture": + crashyfix = crashyfix_using_nursery_fixture + else: + crashyfix = crashyfix_using_manual_nursery + + testdir.makepyfile( + """ + import pytest + from pytest_trio import trio_fixture + import trio + from async_generator import async_generator, yield_ + + teardown_deadlines = {} + final_time = None + + async def crashy(): + await trio.sleep(1) + raise RuntimeError + + CRASHYFIX_HERE + + @trio_fixture + def sidefix(): + yield + # We should NOT be cancelled here + teardown_deadlines["sidefix"] = trio.current_effective_deadline() + + @trio_fixture + def userfix(crashyfix): + yield + # Currently we should NOT be cancelled here... though maybe this + # should change? + teardown_deadlines["userfix"] = trio.current_effective_deadline() + + @pytest.mark.trio + async def test_it(userfix, sidefix, autojump_clock): + try: + await trio.sleep_forever() + finally: + global final_time + final_time = trio.current_time() + + + def test_post(): + assert teardown_deadlines == { + "crashyfix": -float("inf"), + "sidefix": float("inf"), + "userfix": float("inf"), + } + assert final_time == 1 + """ + .replace("CRASHYFIX_HERE", crashyfix) + ) + + result = testdir.runpytest() + result.assert_outcomes(passed=1, failed=1) From 9138b86a2de378acc448932a71a9da6871494564 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Fri, 17 Aug 2018 01:10:44 -0700 Subject: [PATCH 041/185] yapf --- pytest_trio/_tests/test_fixture_ordering.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pytest_trio/_tests/test_fixture_ordering.py b/pytest_trio/_tests/test_fixture_ordering.py index 788d10d..352ca6a 100644 --- a/pytest_trio/_tests/test_fixture_ordering.py +++ b/pytest_trio/_tests/test_fixture_ordering.py @@ -1,5 +1,6 @@ import pytest + # Tests that: # - leaf_fix gets set up first and torn down last # - the two fix_concurrent_{1,2} fixtures run their setup/teardown code @@ -195,13 +196,15 @@ def test_followup(): result = testdir.runpytest() result.assert_outcomes(passed=1, failed=1) - result.stdout.fnmatch_lines_random([ - "*CRASH_NONGEN*", - "*CRASH_EARLY_AGEN*", - "*CRASH_LATE_AGEN*", - "*CRASH_BACKGROUND_EARLY*", - "*CRASH_BACKGROUND_LATE*", - ]) + result.stdout.fnmatch_lines_random( + [ + "*CRASH_NONGEN*", + "*CRASH_EARLY_AGEN*", + "*CRASH_LATE_AGEN*", + "*CRASH_BACKGROUND_EARLY*", + "*CRASH_BACKGROUND_LATE*", + ] + ) @pytest.mark.parametrize("bgmode", ["nursery fixture", "manual nursery"]) @@ -276,8 +279,7 @@ def test_post(): "userfix": float("inf"), } assert final_time == 1 - """ - .replace("CRASHYFIX_HERE", crashyfix) + """.replace("CRASHYFIX_HERE", crashyfix) ) result = testdir.runpytest() From 4d2ec80c6de50b3626027ad4d0432fb9be2f084e Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Fri, 17 Aug 2018 02:51:08 -0700 Subject: [PATCH 042/185] Give the fixture/test tasks meaningful names --- pytest_trio/_tests/test_fixture_names.py | 18 ++++++++++++++++++ pytest_trio/plugin.py | 18 ++++++++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 pytest_trio/_tests/test_fixture_names.py diff --git a/pytest_trio/_tests/test_fixture_names.py b/pytest_trio/_tests/test_fixture_names.py new file mode 100644 index 0000000..f22f64e --- /dev/null +++ b/pytest_trio/_tests/test_fixture_names.py @@ -0,0 +1,18 @@ +import pytest +from pytest_trio import trio_fixture +import trio + + +@trio_fixture +def fixture_with_unique_name(nursery): + nursery.start_soon(trio.sleep_forever) + + +@pytest.mark.trio +async def test_fixture_names(fixture_with_unique_name): + # This might be a bit fragile ... if we rearrange the nursery hierarchy + # somehow so it breaks, then we can make it more robust. + task = trio.hazmat.current_task() + assert task.name == "" + sibling_names = {task.name for task in task.parent_nursery.child_tasks} + assert "" in sibling_names diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 46f4d6a..fa78c89 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -134,7 +134,8 @@ class TrioFixture: dependents and no teardown. """ - def __init__(self, func, pytest_kwargs, is_test=False): + def __init__(self, name, func, pytest_kwargs, is_test=False): + self.name = name self._func = func self._pytest_kwargs = pytest_kwargs self._is_test = is_test @@ -294,11 +295,16 @@ async def _bootstrap_fixtures_and_run_test(**kwargs): __tracebackhide__ = True ctx = TrioTestContext() - test = TrioFixture(testfunc, kwargs, is_test=True) + test = TrioFixture( + "".format(testfunc.__name__), + testfunc, + kwargs, + is_test=True + ) async with trio.open_nursery() as nursery: for fixture in test.register_and_collect_dependencies(): - nursery.start_soon(fixture.run, ctx) + nursery.start_soon(fixture.run, ctx, name=fixture.name) if ctx.error_list: raise trio.MultiError(ctx.error_list) @@ -366,7 +372,11 @@ def handle_fixture(fixturedef, request, force_trio_mode): raise RuntimeError("Trio fixtures must be function-scope") if not is_trio_test: raise RuntimeError("Trio fixtures can only be used by Trio tests") - fixture = TrioFixture(fixturedef.func, kwargs) + fixture = TrioFixture( + "".format(fixturedef.argname), + fixturedef.func, + kwargs, + ) fixturedef.cached_result = (fixture, request.param_index, None) return fixture From fab1c67cf0c3e3452e794c1be355f20e25d42b8b Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Sun, 22 Jul 2018 23:40:56 -0700 Subject: [PATCH 043/185] First draft of manual --- docs/source/index.rst | 42 ++++ docs/source/quickstart.rst | 389 +++++++++++++++++++++++++++++++++++++ docs/source/reference.rst | 176 +++++++++++++++++ 3 files changed, 607 insertions(+) create mode 100644 docs/source/quickstart.rst create mode 100644 docs/source/reference.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 118c45b..1b7af1a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -8,9 +8,51 @@ pytest-trio: Pytest plugin for trio =================================== +This is a pytest plugin to help you test projects that use `Trio +`__, a friendly library for concurrency +and async I/O in Python. Features include: + +* Async tests without the boilerplate: just write ``async def + test_whatever(): ...``. + +* Useful fixtures included: use ``autojump_clock`` for easy testing of + code with timeouts. + +* Write your own async fixtures: start up a Trio-based server inside a + fixture, and then connect to it from your test. + +* Use `Hypothesis `__? This plugin + integrates with Hypothesis, so your async tests can use + property-based testing. + + +Vital statistics +================ + +* Install: ``pip install pytest-trio`` + +* Documentation: https://pytest-trio.readthedocs.io + +* Issue tracker, source code: https://github.com/python-trio/pytest-trio + +* License: MIT or Apache 2, your choice + +* Contributor guide: https://trio.readthedocs.io/en/latest/contributing.html + +* Code of conduct: Contributors are requested to follow our `code of + conduct + `__ in + all project spaces. + .. toctree:: :maxdepth: 2 + quickstart.rst + reference.rst + +.. toctree:: + :maxdepth: 1 + history.rst ==================== diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst new file mode 100644 index 0000000..fb90500 --- /dev/null +++ b/docs/source/quickstart.rst @@ -0,0 +1,389 @@ +Quickstart +========== + +Enabling Trio mode and running your first async tests +----------------------------------------------------- + +.. note:: If you used `cookiecutter-trio + `__ to set up + your project, then pytest-trio and Trio mode are already + configured! You can write ``async def test_whatever(): ...`` and it + should just work. Feel free to skip to the next section. + +Let's make a temporary directory to work in, and write two trivial +tests: one that we expect should pass, and one that we expect should +fail:: + + # test_example.py + import trio + + async def test_sleep(): + start_time = trio.current_time() + await trio.sleep(1) + end_time = trio.current_time() + assert end_time - start_time >= 1 + + async def test_should_fail(): + assert False + +If we run this under pytest normally, then we get a strange result: + +.. code-block:: none + + $ pytest test_example.py + + ======================== test session starts ========================= + platform linux -- Python 3.6.5, pytest-3.6.3, py-1.5.4, pluggy-0.6.0 + rootdir: /tmp, inifile: + collected 2 items + + test_example.py .. [100%] + + ========================== warnings summary ========================== + test_example.py::test_sleep + .../_pytest/python.py:196: RuntimeWarning: coroutine 'test_sleep' was never awaited + testfunction(**testargs) + + test_example.py::test_should_fail + .../_pytest/python.py:196: RuntimeWarning: coroutine 'test_should_fail' was never awaited + testfunction(**testargs) + + -- Docs: http://doc.pytest.org/en/latest/warnings.html + ================ 2 passed, 2 warnings in 0.02 seconds ================ + +So ``test_sleep`` passed, which is what we expected... but +``test_should_fail`` also passes, which is strange. And it says that +the whole test run completed in 0.02 seconds, which is weird, because +``test_sleep`` should have taken at least second to run. And then +there are these strange warnings at the bottom... what's going on +here? + +The problem is that our tests are async, and pytest doesn't know what +to do with it. So it basically skips running them entirely, and then +reports them as passed. This is not very helpful! If you see warnings +like this, or if your tests seem to pass but your coverage reports +claim that they weren't run at all, then this might be the problem. + +Here's the fix: + +1. Install pytest-trio: ``pip install pytest-trio`` + +2. In your project root, create a file called ``pytest.ini`` with + contents: + + .. code-block:: none + + [pytest] + trio_mode = true + +And we're done! Let's try running pytest again: + +.. code-block:: none + + $ pip install pytest-trio + + $ cat <pytest.ini + [pytest] + trio_mode = true + EOF + + $ pytest test_example.py + ======================== test session starts ========================= + platform linux -- Python 3.6.5, pytest-3.6.3, py-1.5.4, pluggy-0.6.0 + rootdir: /tmp, inifile: pytest.ini + plugins: trio-0.4.2 + collected 2 items + + test_example.py .F [100%] + + ============================== FAILURES ============================== + __________________________ test_should_fail __________________________ + + async def test_should_fail(): + > assert False + E assert False + + test_example.py:7: AssertionError + ================= 1 failed, 1 passed in 1.05 seconds ================= + +Notice that now it says ``plugins: trio``, which means that +pytest-trio is installed, and the results make sense: the good test +passed, the bad test failed, no warnings, and it took just over 1 +second, like we'd expect. + + +Trio's magic autojump clock +--------------------------- + +Tests involving time are often slow and flaky. But we can +fix that. Just add the ``autojump_clock`` fixture to your test, and +it will run in a mode where Trio's clock is virtualized and +deterministic. Essentially, the clock doesn't move, except that whenever all +tasks are blocked waiting, it jumps forward until the next time when +something will happen:: + + # Notice the 'autojump_clock' argument: that's all it takes! + async def test_sleep_efficiently_and_reliably(autojump_clock): + start_time = trio.current_time() + await trio.sleep(1) + end_time = trio.current_time() + assert start_time - end_time == 1 + +In the version of this test we saw before that used real time, at the +end we had to use a ``>=`` comparison, in order to account for +scheduler jitter and so forth. If there were a bug that caused +:func:`trio.sleep` to take 10 seconds, our test wouldn't have noticed. +But now we're using virtual time, so the call to ``await +trio.sleep(1)`` takes *exactly* 1 virtual second, and the ``==`` test +will pass every time. Before, we had to wait around for the test to +complete; now, it completes essentially instantaneously. (Try it!) +And, while here our example is super simple, it's integration with +Trio's core scheduling logic allows this to work for arbitrarily +complex programs (as long as they aren't interacting with the outside +world). + + +Async fixtures +-------------- + +Now that we have ``trio_mode`` activated, we can also write async +fixtures:: + + import pytest + import trio + + @pytest.fixture + async def fixture_that_sleeps_for_some_reason(): + await trio.sleep(1) + return "slept" + + async def test_example(fixture_that_sleeps_for_some_reason): + assert fixture_that_sleeps_for_some_reason == "slept" + +If you need to run teardown code, you can use ``yield``, just like a +regular pytest fixture:: + + @pytest.fixture + async def connection_to_httpbin(): + # Setup code + stream = await trio.open_tcp_stream("httpbin", 80) + + # The value of this fixture + yield stream + + # Teardown code, executed after the test is done + await stream.close() + + +.. _server-fixture-example: + +Running a background server from a fixture +------------------------------------------ + +Here's some code to implement an echo server. It's supposed to take in +arbitrary data, and then send it back out again:: + + async def echo_server_handler(stream): + while True: + data = await stream.receive_some(1000) + if not data: + break + await stream.send_all(data) + + # Usage: await trio.serve_tcp(echo_server_handler, ...) + +Since this is such complicated and sophisticated code, we want to +write lots of tests to make sure it's working correctly. To test it, +we need to start one task to run the echo server, and then connect to +it from a second task and send it test data. Here's a first attempt:: + + # Let's cross our fingers and hope no-one else is using this port... + PORT = 14923 + + # Don't copy this -- we can do better + async def test_attempt_1(): + async with trio.open_nursery() as nursery: + # Start server running in the background + nursery.start_soon( + partial(trio.serve_tcp, echo_server_handler, port=PORT) + ) + + # Connect to the server. + client = await trio.open_tcp_stream("127.0.0.1", PORT) + # Send some test data, and check that it gets echoed back + async with client: + for test_byte in [b"a", b"c", b"c"]: + await client.send_all(test_byte) + assert await client.receive_some(1) == test_byte + +This will mostly work, but it has a few problems. The first one is +that there's a race condition: We spawn a background task to call +``serve_tcp``, and then immediately try to connect to that server. +Sometimes this will work fine. But it takes a little while for the +server to start up and be ready to accept connections – so other +times, randomly, our connection attempt will happen too quickly, and +error out. After all – ``nursery.start_soon`` only promises that the +task will be started *soon*, not that it's actually happened. + +To solve this race condition, we should switch to using ``await +nursery.start(...)``. You should `read its docs for full details +`__, +but basically the idea is that this starts up a background task, and +waits for it to finish getting started before returning. This requires +some cooperation from the function you're calling: it has to tell +``nursery.start`` when it's finished setting up. Fortunately, +:func:`trio.serve_tcp` is already set up to cooperate with +``nursery.start``, so we can write:: + + # Let's cross our fingers and hope no-one else is using this port... + PORT = 14923 + + # Don't copy this -- we can do better + async def test_attempt_2(): + async with trio.open_nursery() as nursery: + # Start server running in the background + # AND wait for it to finish starting up before continuing + await nursery.start( + partial(trio.serve_tcp, echo_server_handler, port=PORT) + ) + + # Connect to the server + client = await trio.open_tcp_stream("127.0.0.1", PORT) + async with client: + for test_byte in [b"a", b"c", b"c"]: + await client.send_all(test_byte) + assert await client.receive_some(1) == test_byte + +That solves our race condition. Next issue: hardcoding the port number +like this is a bad idea, because port numbers are a machine-wide +resource, so if we're unlucky some other program might already be +using it. What we really want to do is to tell :func:`~trio.serve_tcp` +to pick a random port that no-one else is using. It turns out that +this is easy: if you request port 0, then the operating system instead +picks an unused one for you automatically. Problem solved! + +But wait... if the operating system is picking the port for us, how do +we know figure out which one it picked, so we can connect? + +Well, there's no way to predict the port ahead of time. But after +:func:`~trio.serve_tcp` has opened a port, it can check and see what +it got. So we need some way to pass this data back out of +:func:`~trio.serve_tcp`. Fortunately, ``nursery.start`` handles this +too: it lets the task pass out a piece of data after it's started. And +it just so happens that what :func:`~trio.serve_tcp` passes out is a +list of :class:`~trio.SocketListener` objects. And there's a handy +function called :func:`trio.testing.open_stream_to_socket_listener` +that can take a :class:`~trio.SocketListener` and make a connection to +it. + +Putting it all together:: + + from trio.testing import open_stream_to_socket_listener + + # Don't copy this -- we can do better + async def test_attempt_3(): + async with trio.open_nursery() as nursery: + # Start server running in the background + # AND wait for it to finish starting up before continuing + # AND find out where it's actually listening + listeners = await nursery.start( + partial(trio.serve_tcp, echo_server_handler, port=0) + ) + + # Connect to the server. + # There might be multiple listeners (example: IPv4 and + # IPv6), but we don't care which one we connect to, so we + # just use the first. + client = await open_stream_to_socket_listener(listeners[0]) + async with client: + for test_byte in [b"a", b"c", b"c"]: + await client.send_all(test_byte) + assert await client.receive_some(1) == test_byte + +Okay, this is getting closer... but if we try to run it, we'll find +that it just hangs instead of completing. What's going on? + +The problem is that after we finish doing our tests, we need to shut +down the server – otherwise it will keep waiting for new connections +forever, and the test will never finish. We can fix this by cancelling +the nursery once we're done with it:: + + # Don't copy this -- it finally works, but we can still do better! + async def test_attempt_4(): + async with trio.open_nursery() as nursery: + # Start server running in the background + # AND wait for it to finish starting up before continuing + # AND find out where it's actually listening + listeners = await nursery.start( + partial(trio.serve_tcp, echo_server_handler, port=0) + ) + + # Connect to the server. + # There might be multiple listeners (example: IPv4 and + # IPv6), but we don't care which one we connect to, so we + # just use the first. + client = await open_stream_to_socket_listener(listeners[0]) + async with client: + for test_byte in [b"a", b"c", b"c"]: + await client.send_all(test_byte) + assert await client.receive_some(1) == test_byte + + # Shut down the server now that we're done testing it + nursery.cancel_scope.cancel() + +Okay, finally this test works correctly. But that's a lot of +boilerplate. Remember, we need to write lots of tests for this server, +and we don't want to have to copy-paste all that stuff into every +test. Let's factor it out into a fixture. + +Probably our first attempt will look something like:: + + # DON'T DO THIS, IT DOESN'T WORK + @pytest.fixture + async def echo_server_connection(): + async with trio.open_nursery() as nursery: + await nursery.start(...) + yield ... + nursery.cancel_scope.cancel() + +Unfortunately, this doesn't work. **You cannot make a fixture that +opens a nursery, and then yields from inside the nursery block.** +Sorry. + +Instead, pytest-trio provides a built-in fixture called +``test_nursery``. This is a nursery that pytest-trio creates +internally, that lasts for as long as the test is running, and then +pytest-trio cancels it. Which is exactly what we need – in fact it's +even simpler than our first try, because now we don't need to worry +about cancelling the nursery ourselves. + +So here's our complete, final version:: + + # Final version -- copy this! + from functools import partial + import pytest + import trio + from trio.testing import open_stream_to_socket_listener + + async def echo_server_handler(stream): + while True: + data = await stream.receive_some(1000) + if not data: + break + await stream.send_all(data) + + @pytest.fixture + async def echo_server_connection(test_nursery): + listeners = await test_nursery.start( + partial(trio.serve_tcp, echo_server_handler, port=0) + ) + client = await open_stream_to_socket_listener(listeners[0]) + async with client: + yield client + + async def test_final(echo_server_connection): + for test_byte in [b"a", b"c", b"c"]: + await echo_server_connection.send_all(test_byte) + assert await echo_server_connection.receive_some(1) == test_byte + +No race conditions, no hangs, simple, clean, and reusable. diff --git a/docs/source/reference.rst b/docs/source/reference.rst new file mode 100644 index 0000000..c23a1d3 --- /dev/null +++ b/docs/source/reference.rst @@ -0,0 +1,176 @@ +Reference +========= + +Trio mode +--------- + +Most users will want to enable "Trio mode". Without Trio mode: + +* Async tests have to be decorated with ``@pytest.mark.trio`` +* Async fixtures have to be decorated with + ``@pytest_trio.trio_fixture`` (instead of ``@pytest.fixture``). + +When Trio mode is enabled, two things happen: + +* Async tests automatically have the ``trio`` mark added, so you don't + have to do it yourself. +* Async fixtures using ``@pytest.fixture`` automatically get converted + to Trio fixtures. + +There are two ways to enable Trio mode. + +The first option is to **use a pytest configuration file**. The exact +rules for how pytest finds configuration files are `a bit complicated +`__, but you want to +end up with something like: + +.. code-block:: ini + + # pytest.ini + [pytest] + trio_mode = true + +The second option is **use a conftest.py file**. Inside your tests +directory, create a file called ``conftest.py``, with the following +contents:: + + # conftest.py + from pytest_trio.enable_trio_mode import * + +This does exactly the same thing as setting ``trio_mode = true`` in +``pytest.ini``, except for two things: + +* Some people like to ship their tests as part of their library, so + they (or their users) can test the final installed software by + running ``pytest --pyargs PACKAGENAME``. In this mode, + ``pytest.ini`` files don't work, but ``conftest.py`` files do. + +* Enabling Trio mode in ``pytest.ini`` always enables it globally for + your entire testsuite. Enabling it in ``conftest.py`` only enables + it for test files that are in the same directory as the + ``conftest.py``, or its subdirectories. + +If you have software that uses multiple async libraries, then you can +use ``conftest.py`` to enable Trio mode for just the part of your +testsuite that uses Trio; or, if you need even finer-grained control, +you can leave Trio mode disabled and use ``@pytest.mark.trio`` +explicitly on all your Trio tests. + + +Trio fixtures +------------- + +Normally, pytest runs fixture code before starting the test, and +teardown code afterwards. For technical reasons, we can't wrap this +whole process in :func:`trio.run` – only the test itself. As a +workaround, pytest-trio introduces the concept of a "Trio fixture", +which acts like a normal fixture for most purposes, but actually does +the setup and teardown inside the test's call to :func:`trio.run`. + +The following fixtures are treated as Trio fixtures: + +* Any function decorated with ``@pytest_trio.trio_fixture``. +* If Trio mode is enabled, then any *async* function decorated with + ``@pytest.fixture`` automatically becomes a Trio fixture. +* Any fixture which depends on a Trio fixture. + +The most notable difference between regular fixtures and Trio fixtures +is that regular fixtures can't use Trio APIs, but Trio fixtures can. +Most of the time you don't need to worry about this, because you +normally only call Trio APIs from async functions, and when Trio mode +is enabled, all async fixtures are automatically Trio fixtures. +However, if for some reason you do want to use Trio APIs from a +synchronous fixture, then you'll have to use +``@pytest_trio.trio_fixture``:: + + # This fixture is not very useful + # But it is an example where @pytest.fixture doesn't work + @pytest_trio.trio_fixture + def trio_time(): + return trio.current_time() + +Only Trio tests can use Trio fixtures. If you have a regular +(synchronous) test that tries to use a Trio fixture, then that's an +error. + +And finally, regular fixtures can be `scoped to the test, class, +module, or session +`__, +but Trio fixtures **must be test scoped**. Class, module, and session +scope are not supported. + + +Built-in fixtures +----------------- + +These fixtures are automatically available to any code using +pytest-trio. + +.. data:: autojump_clock + + A :class:`trio.testing.MockClock`, configured with ``rate=0, + autojump_threshold=0``. + +.. data:: mock_clock + + A :class:`trio.testing.MockClock`, with its default configuration + (``rate=0, autojump_threshold=inf``). + +What makes these particularly useful is that whenever pytest-trio runs +a test, it checks the fixtures to see if one of them is a +:class:`trio.abc.Clock` object. If so, it passes that object to +:func:`trio.run`. So if your test requests one of these fixtures, it +automatically uses that clock. + +If you implement your own :class:`~trio.abc.Clock`, and implement a +fixture that returns it, then it will work the same way. + +Of course, like any pytest fixture, you also get the actual object +available. For example, you can call +:meth:`~trio.testing.MockClock.jump`:: + + async def test_time_travel(mock_clock): + assert trio.current_time() == 0 + mock_clock.jump(10) + assert trio.current_time() == 10 + +.. data:: test_nursery + + A nursery created and managed by pytest-trio itself. When + pytest-trio runs a test, it performs these steps in this order: + + 1. Open the ``test_nursery`` + 2. Set up all Trio fixtures. + 3. Run the test. + 4. Tear down all Trio fixtures. + 5. Cancel the ``test_nursery``. + + See :ref:`server-fixture-example` for an example of how this can be + used. + + +Integration with the Hypothesis library +--------------------------------------- + +There isn't too much to say here, since the obvious thing just works:: + + from hypothesis import given + import hypothesis.strategies as st + + @given(st.binary()) + async def test_trio_and_hypothesis(data): + ... + +Under the hood, this requires some coordination between Hypothesis and +pytest-trio. Hypothesis runs your test multiple times with different +examples of random data. For each example, pytest-trio calls +:func:`trio.run` again (so you get a fresh clean Trio environment), +sets up any Trio fixtures, runs the actual test, and then tears down +any Trio fixtures. Most of the time this shouldn't matter (and `is +probably what you want anyway +`__), but in +some unusual cases it could surprise you. And note that this only +applies to Trio fixtures – if a Trio test uses a mix of Trio fixtures +and regular fixtures, then the regular fixtures will still only +`instantiated once and re-used for all examples +`__. From 0217e26a537a80ca671c7763498e7b55d666e239 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Mon, 23 Jul 2018 22:48:38 -0700 Subject: [PATCH 044/185] Tweak fixture semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fixture setup code doesn't have convenient access to config file settings, and also keying the automagic-async-fixture-handling off of the trio-mode setting would be a compatibility break. Not the end of the world, but kind of annoying. Instead, let's continue to key the automagic-async-fixture-handling off of the requesting test being a Trio test. It's just as convenient, and while technically could be a little more error-prone – specifically, if you accidentally use an async fixture with a non-async test, then we won't be able to catch that as an error – I doubt it matters much in practice. And if people turn out to make that mistake a lot, we can always add some check for it later. --- docs/source/reference.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/reference.rst b/docs/source/reference.rst index c23a1d3..ba0d5c9 100644 --- a/docs/source/reference.rst +++ b/docs/source/reference.rst @@ -70,8 +70,8 @@ the setup and teardown inside the test's call to :func:`trio.run`. The following fixtures are treated as Trio fixtures: * Any function decorated with ``@pytest_trio.trio_fixture``. -* If Trio mode is enabled, then any *async* function decorated with - ``@pytest.fixture`` automatically becomes a Trio fixture. +* Any async function decorated with ``@pytest.fixture``, *if* + requested by a Trio test. * Any fixture which depends on a Trio fixture. The most notable difference between regular fixtures and Trio fixtures From e5289d015bd5f6b8f615db174dd748bac0af2779 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Tue, 24 Jul 2018 20:29:25 -0700 Subject: [PATCH 045/185] Edits after review --- docs/source/index.rst | 10 ++--- docs/source/quickstart.rst | 78 ++++++++++++++++++++------------------ docs/source/reference.rst | 36 ++++++++++++------ 3 files changed, 70 insertions(+), 54 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 1b7af1a..4682c05 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -18,12 +18,12 @@ and async I/O in Python. Features include: * Useful fixtures included: use ``autojump_clock`` for easy testing of code with timeouts. -* Write your own async fixtures: start up a Trio-based server inside a - fixture, and then connect to it from your test. +* Write your own async fixtures: set up an async database connection + or start a server inside a fixture, and then use it in your tests. -* Use `Hypothesis `__? This plugin - integrates with Hypothesis, so your async tests can use - property-based testing. +* Integration with the fabulous `Hypothesis + `__ library, so your async tests can use + property-based testing: just use ``@given`` like you're used to. Vital statistics diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index fb90500..4fa313d 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -146,33 +146,31 @@ world). Async fixtures -------------- -Now that we have ``trio_mode`` activated, we can also write async -fixtures:: - - import pytest - import trio +We can write async fixtures:: @pytest.fixture - async def fixture_that_sleeps_for_some_reason(): - await trio.sleep(1) - return "slept" + async def db_connection(): + return await some_async_db_library.connect(...) - async def test_example(fixture_that_sleeps_for_some_reason): - assert fixture_that_sleeps_for_some_reason == "slept" + async def test_example(db_connection): + await db_connection.execute("SELECT * FROM ...") If you need to run teardown code, you can use ``yield``, just like a regular pytest fixture:: + # DB connection that wraps each test in a transaction and rolls it + # back afterwards @pytest.fixture - async def connection_to_httpbin(): + async def rolback_db_connection(): # Setup code - stream = await trio.open_tcp_stream("httpbin", 80) + connection = await some_async_db_library.connect(...) + await connection.execute("START TRANSACTION") # The value of this fixture - yield stream + yield connection # Teardown code, executed after the test is done - await stream.close() + await connection.execute("ROLLBACK") .. _server-fixture-example: @@ -194,8 +192,9 @@ arbitrary data, and then send it back out again:: Since this is such complicated and sophisticated code, we want to write lots of tests to make sure it's working correctly. To test it, -we need to start one task to run the echo server, and then connect to -it from a second task and send it test data. Here's a first attempt:: +we want to start a background task running the echo server itself, and +then we'll connect to it, send it test data, and see how it responds. +Here's a first attempt:: # Let's cross our fingers and hope no-one else is using this port... PORT = 14923 @@ -226,14 +225,15 @@ error out. After all – ``nursery.start_soon`` only promises that the task will be started *soon*, not that it's actually happened. To solve this race condition, we should switch to using ``await -nursery.start(...)``. You should `read its docs for full details +nursery.start(...)``. You can `read its docs for full details `__, -but basically the idea is that this starts up a background task, and -waits for it to finish getting started before returning. This requires -some cooperation from the function you're calling: it has to tell -``nursery.start`` when it's finished setting up. Fortunately, -:func:`trio.serve_tcp` is already set up to cooperate with -``nursery.start``, so we can write:: +but basically the idea is that both ``nursery.start_soon(...)`` and +``await nursery.start(...)`` create background tasks, but only +``start`` waits for the new task to finish getting itself set up. This +requires some cooperation from the background task: it has to notify +``nursery.start`` when it's ready. Fortunately, :func:`trio.serve_tcp` +already knows how to cooperated with ``nursery.start``, so we can +write:: # Let's cross our fingers and hope no-one else is using this port... PORT = 14923 @@ -259,11 +259,11 @@ like this is a bad idea, because port numbers are a machine-wide resource, so if we're unlucky some other program might already be using it. What we really want to do is to tell :func:`~trio.serve_tcp` to pick a random port that no-one else is using. It turns out that -this is easy: if you request port 0, then the operating system instead -picks an unused one for you automatically. Problem solved! +this is easy: if you request port 0, then the operating system will +pick an unused one for you automatically. Problem solved! But wait... if the operating system is picking the port for us, how do -we know figure out which one it picked, so we can connect? +we know figure out which one it picked, so we can connect to it later? Well, there's no way to predict the port ahead of time. But after :func:`~trio.serve_tcp` has opened a port, it can check and see what @@ -303,10 +303,10 @@ Putting it all together:: Okay, this is getting closer... but if we try to run it, we'll find that it just hangs instead of completing. What's going on? -The problem is that after we finish doing our tests, we need to shut -down the server – otherwise it will keep waiting for new connections -forever, and the test will never finish. We can fix this by cancelling -the nursery once we're done with it:: +The problem is that we need to shut down the server – otherwise it +will keep waiting for new connections forever, and the test will never +finish. We can fix this by cancelling everything in the nursery once +our test is finished:: # Don't copy this -- it finally works, but we can still do better! async def test_attempt_4(): @@ -347,15 +347,16 @@ Probably our first attempt will look something like:: nursery.cancel_scope.cancel() Unfortunately, this doesn't work. **You cannot make a fixture that -opens a nursery, and then yields from inside the nursery block.** -Sorry. +opens a nursery or cancel scope, and then yields from inside the +nursery or cancel scope block.** Sorry 🙁. We're `working on it +`__. Instead, pytest-trio provides a built-in fixture called -``test_nursery``. This is a nursery that pytest-trio creates -internally, that lasts for as long as the test is running, and then -pytest-trio cancels it. Which is exactly what we need – in fact it's -even simpler than our first try, because now we don't need to worry -about cancelling the nursery ourselves. +``test_nursery``. This is a nursery that's created before each test, +and then automatically cancelled after the test finishes. Which is +exactly what we need – in fact it's even simpler than our first try, +because now we don't need to worry about cancelling the nursery +ourselves. So here's our complete, final version:: @@ -365,6 +366,7 @@ So here's our complete, final version:: import trio from trio.testing import open_stream_to_socket_listener + # The code being tested: async def echo_server_handler(stream): while True: data = await stream.receive_some(1000) @@ -372,6 +374,7 @@ So here's our complete, final version:: break await stream.send_all(data) + # The fixture: @pytest.fixture async def echo_server_connection(test_nursery): listeners = await test_nursery.start( @@ -381,6 +384,7 @@ So here's our complete, final version:: async with client: yield client + # A test using the fixture: async def test_final(echo_server_connection): for test_byte in [b"a", b"c", b"c"]: await echo_server_connection.send_all(test_byte) diff --git a/docs/source/reference.rst b/docs/source/reference.rst index ba0d5c9..ed64f3d 100644 --- a/docs/source/reference.rst +++ b/docs/source/reference.rst @@ -6,16 +6,21 @@ Trio mode Most users will want to enable "Trio mode". Without Trio mode: -* Async tests have to be decorated with ``@pytest.mark.trio`` -* Async fixtures have to be decorated with - ``@pytest_trio.trio_fixture`` (instead of ``@pytest.fixture``). +* Pytest-trio only handles tests that have been decorated with + ``@pytest.mark.trio`` +* Pytest-trio only handles fixtures if they're async *and* used by a + test that's decorated with ``@pytest.mark.trio``, or if they're + decorated with ``@pytest_trio.trio_fixture`` (instead of + ``@pytest.fixture``). -When Trio mode is enabled, two things happen: +When Trio mode is enabled, two extra things happen: * Async tests automatically have the ``trio`` mark added, so you don't have to do it yourself. * Async fixtures using ``@pytest.fixture`` automatically get converted - to Trio fixtures. + to Trio fixtures. (The main effect of this is that it helps you + catch mistakes where a you use an async fixture with a non-async + test.) There are two ways to enable Trio mode. @@ -166,11 +171,18 @@ pytest-trio. Hypothesis runs your test multiple times with different examples of random data. For each example, pytest-trio calls :func:`trio.run` again (so you get a fresh clean Trio environment), sets up any Trio fixtures, runs the actual test, and then tears down -any Trio fixtures. Most of the time this shouldn't matter (and `is -probably what you want anyway +any Trio fixtures. Notice that this is a bit different than regular +pytest fixtures, which are `instantiated once and then re-used for all +`__. Most of the time +this shouldn't matter (and `is probably what you want anyway `__), but in -some unusual cases it could surprise you. And note that this only -applies to Trio fixtures – if a Trio test uses a mix of Trio fixtures -and regular fixtures, then the regular fixtures will still only -`instantiated once and re-used for all examples -`__. +some unusual cases it could surprise you. And this only applies to +Trio fixtures – if a Trio test uses a mix of regular fixtures and Trio +fixtures, then the regular fixtures will be reused, while the Trio +fixtures will be repeatedly reinstantiated. + +Also, pytest-trio only handles ``@given``\-based tests. If you want to +write `stateful tests +`__ for +Trio-based libraries, then check out `hypothesis-trio +`__. From 2165d1061c42ef930b094a14c484a87e0fcdcf52 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Tue, 24 Jul 2018 20:59:54 -0700 Subject: [PATCH 046/185] Catch up the reference docs on what counts as a trio fixture --- docs/source/reference.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/reference.rst b/docs/source/reference.rst index ed64f3d..1021093 100644 --- a/docs/source/reference.rst +++ b/docs/source/reference.rst @@ -76,7 +76,8 @@ The following fixtures are treated as Trio fixtures: * Any function decorated with ``@pytest_trio.trio_fixture``. * Any async function decorated with ``@pytest.fixture``, *if* - requested by a Trio test. + Trio mode is enabled *or* this fixture is being requested by a Trio + test. * Any fixture which depends on a Trio fixture. The most notable difference between regular fixtures and Trio fixtures From 8660bb22dc3826d8ffb96af1aabdac665aeeb2e4 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Tue, 24 Jul 2018 21:06:31 -0700 Subject: [PATCH 047/185] Speling --- docs/source/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 4fa313d..04b3cc1 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -161,7 +161,7 @@ regular pytest fixture:: # DB connection that wraps each test in a transaction and rolls it # back afterwards @pytest.fixture - async def rolback_db_connection(): + async def rollback_db_connection(): # Setup code connection = await some_async_db_library.connect(...) await connection.execute("START TRANSACTION") From e52357e1e8e4d0bd2b9fd3daafe3250c725af7c3 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Fri, 17 Aug 2018 02:59:51 -0700 Subject: [PATCH 048/185] Don't call it Pytest_Trio that just looks silly --- docs/source/history.rst | 14 +++++++------- pyproject.toml | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/source/history.rst b/docs/source/history.rst index 38c7930..a27ad57 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -5,7 +5,7 @@ Release history .. towncrier release notes start -Pytest_Trio 0.4.2 (2018-06-29) +pytest-trio 0.4.2 (2018-06-29) ------------------------------ Features @@ -16,20 +16,20 @@ Features using Trio. (`#42 `__) -Pytest_Trio 0.4.1 (2018-04-14) +pytest-trio 0.4.1 (2018-04-14) ------------------------------ No significant changes. -Pytest_Trio 0.4.0 (2018-04-14) +pytest-trio 0.4.0 (2018-04-14) ------------------------------ - Fix compatibility with trio 0.4.0 (`#25 `__) -Pytest_Trio 0.3.0 (2018-01-03) +pytest-trio 0.3.0 (2018-01-03) ------------------------------ Features @@ -39,7 +39,7 @@ Features `__) -Pytest_Trio 0.2.0 (2017-12-15) +pytest-trio 0.2.0 (2017-12-15) ------------------------------ - Heavy improvements, add async yield fixture, fix bugs, add tests etc. (`#17 @@ -53,14 +53,14 @@ Deprecations and Removals `__) -Pytest_Trio 0.1.1 (2017-12-08) +pytest-trio 0.1.1 (2017-12-08) ------------------------------ Disable intersphinx for trio (cause crash in CI for the moment due to 404 in readthedoc). -Pytest_Trio 0.1.0 (2017-12-08) +pytest-trio 0.1.0 (2017-12-08) ------------------------------ Initial release. diff --git a/pyproject.toml b/pyproject.toml index 77448c7..407fb8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,5 +2,6 @@ package = "pytest_trio" filename = "docs/source/history.rst" directory = "newsfragments" +title_format = "pytest-trio {version} ({project_date})" underlines = ["-", "~", "^"] issue_format = "`#{issue} `__" From 735b68e9536182d5c4f8dcca5fc0c3e25ada06d2 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Fri, 17 Aug 2018 03:55:51 -0700 Subject: [PATCH 049/185] Update docs for the latest changes in gh-50 Also wrote some draft release notes --- docs/source/history.rst | 20 +++++ docs/source/index.rst | 10 ++- docs/source/quickstart.rst | 148 +++++++++++++++++++------------------ docs/source/reference.rst | 74 ++++++++++++++++--- 4 files changed, 170 insertions(+), 82 deletions(-) diff --git a/docs/source/history.rst b/docs/source/history.rst index a27ad57..2f8fce7 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -5,6 +5,26 @@ Release history .. towncrier release notes start +pytest-trio 0.5.0 (????-??-??) +------------------------------ + +This is a major release, including a rewrite of large portions of the +internals. We believe it should be backwards compatible with existing +projects, but major new features include: + +* "trio mode": no more writing ``@pytest.mark.trio`` everywhere! +* it's now safe to use nurseries inside fixtures (`#55 + `__) +* new ``@trio_fixture`` decorator to explicitly mark a fixture as a + trio fixture +* a number of easy-to-make mistakes are now caught and raise + informative errors +* the :data:`nursery` fixture is now 87% more magical + +For more details, see the manual. Oh right, speaking of which: we +finally have a manual! You should read it. + + pytest-trio 0.4.2 (2018-06-29) ------------------------------ diff --git a/docs/source/index.rst b/docs/source/index.rst index 4682c05..9af200c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -15,16 +15,22 @@ and async I/O in Python. Features include: * Async tests without the boilerplate: just write ``async def test_whatever(): ...``. -* Useful fixtures included: use ``autojump_clock`` for easy testing of - code with timeouts. +* Useful fixtures included: use :data:`autojump_clock` for easy + testing of code with timeouts, or :data:`nursery` to easily set up + background tasks. * Write your own async fixtures: set up an async database connection or start a server inside a fixture, and then use it in your tests. + If you have multiple async fixtures, pytest-trio will even do + setup/teardown concurrently whenever possible. * Integration with the fabulous `Hypothesis `__ library, so your async tests can use property-based testing: just use ``@given`` like you're used to. +* Supports testing projects that use Trio exclusively, and also + projects that support multiple async libraries. + Vital statistics ================ diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 04b3cc1..1c0682d 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -172,6 +172,13 @@ regular pytest fixture:: # Teardown code, executed after the test is done await connection.execute("ROLLBACK") +If you need to support Python 3.5, which doesn't allow ``yield`` +inside an ``async def`` function, then you can define async fixtures +using the `async_generator +`__ +library – just make sure to put the ``@pytest.fixture`` *above* the +``@async_generator``. + .. _server-fixture-example: @@ -191,10 +198,11 @@ arbitrary data, and then send it back out again:: # Usage: await trio.serve_tcp(echo_server_handler, ...) Since this is such complicated and sophisticated code, we want to -write lots of tests to make sure it's working correctly. To test it, -we want to start a background task running the echo server itself, and -then we'll connect to it, send it test data, and see how it responds. -Here's a first attempt:: +write lots of tests to make sure it's working correctly. Our basic +strategy for each test will be to start the echo server running in a +background task, and then in the test body we'll connect to our +server, send it some test data, and see how it responds. Here's a +first attempt:: # Let's cross our fingers and hope no-one else is using this port... PORT = 14923 @@ -208,12 +216,12 @@ Here's a first attempt:: ) # Connect to the server. - client = await trio.open_tcp_stream("127.0.0.1", PORT) + echo_client = await trio.open_tcp_stream("127.0.0.1", PORT) # Send some test data, and check that it gets echoed back - async with client: - for test_byte in [b"a", b"c", b"c"]: - await client.send_all(test_byte) - assert await client.receive_some(1) == test_byte + async with echo_client: + for test_byte in [b"a", b"b", b"c"]: + await echo_client.send_all(test_byte) + assert await echo_client.receive_some(1) == test_byte This will mostly work, but it has a few problems. The first one is that there's a race condition: We spawn a background task to call @@ -232,7 +240,7 @@ but basically the idea is that both ``nursery.start_soon(...)`` and ``start`` waits for the new task to finish getting itself set up. This requires some cooperation from the background task: it has to notify ``nursery.start`` when it's ready. Fortunately, :func:`trio.serve_tcp` -already knows how to cooperated with ``nursery.start``, so we can +already knows how to cooperate with ``nursery.start``, so we can write:: # Let's cross our fingers and hope no-one else is using this port... @@ -248,11 +256,11 @@ write:: ) # Connect to the server - client = await trio.open_tcp_stream("127.0.0.1", PORT) - async with client: - for test_byte in [b"a", b"c", b"c"]: - await client.send_all(test_byte) - assert await client.receive_some(1) == test_byte + echo_client = await trio.open_tcp_stream("127.0.0.1", PORT) + async with echo_client: + for test_byte in [b"a", b"b", b"c"]: + await echo_client.send_all(test_byte) + assert await echo_client.receive_some(1) == test_byte That solves our race condition. Next issue: hardcoding the port number like this is a bad idea, because port numbers are a machine-wide @@ -294,11 +302,11 @@ Putting it all together:: # There might be multiple listeners (example: IPv4 and # IPv6), but we don't care which one we connect to, so we # just use the first. - client = await open_stream_to_socket_listener(listeners[0]) - async with client: - for test_byte in [b"a", b"c", b"c"]: - await client.send_all(test_byte) - assert await client.receive_some(1) == test_byte + echo_client = await open_stream_to_socket_listener(listeners[0]) + async with echo_client: + for test_byte in [b"a", b"b", b"c"]: + await echo_client.send_all(test_byte) + assert await echo_client.receive_some(1) == test_byte Okay, this is getting closer... but if we try to run it, we'll find that it just hangs instead of completing. What's going on? @@ -311,54 +319,54 @@ our test is finished:: # Don't copy this -- it finally works, but we can still do better! async def test_attempt_4(): async with trio.open_nursery() as nursery: - # Start server running in the background - # AND wait for it to finish starting up before continuing - # AND find out where it's actually listening - listeners = await nursery.start( - partial(trio.serve_tcp, echo_server_handler, port=0) - ) - - # Connect to the server. - # There might be multiple listeners (example: IPv4 and - # IPv6), but we don't care which one we connect to, so we - # just use the first. - client = await open_stream_to_socket_listener(listeners[0]) - async with client: - for test_byte in [b"a", b"c", b"c"]: - await client.send_all(test_byte) - assert await client.receive_some(1) == test_byte - - # Shut down the server now that we're done testing it - nursery.cancel_scope.cancel() + try: + listeners = await nursery.start( + partial(trio.serve_tcp, echo_server_handler, port=0) + ) + echo_client = await open_stream_to_socket_listener(listeners[0]) + async with echo_client: + for test_byte in [b"a", b"b", b"c"]: + await echo_client.send_all(test_byte) + assert await echo_client.receive_some(1) == test_byte + finally: + # Shut down the server now that we're done testing it + nursery.cancel_scope.cancel() Okay, finally this test works correctly. But that's a lot of -boilerplate. Remember, we need to write lots of tests for this server, -and we don't want to have to copy-paste all that stuff into every -test. Let's factor it out into a fixture. +boilerplate. Can we slim it down? We can! -Probably our first attempt will look something like:: +First, pytest-trio provides a magical fixture that takes care of the +nursery management boilerplate: just request the :data:`nursery` fixture, +and you get a nursery object that's already set up, and will be +automatically cancelled when you're done:: - # DON'T DO THIS, IT DOESN'T WORK - @pytest.fixture - async def echo_server_connection(): - async with trio.open_nursery() as nursery: - await nursery.start(...) - yield ... - nursery.cancel_scope.cancel() + # Don't copy this -- it finally works, but we can *still* do better! + async def test_attempt_5(nursery): + listeners = await nursery.start( + partial(trio.serve_tcp, echo_server_handler, port=0) + ) + echo_echo_client = await open_stream_to_socket_listener(listeners[0]) + async with echo_client: + for test_byte in [b"a", b"b", b"c"]: + await echo_echo_client.send_all(test_byte) + assert await echo_echo_client.receive_some(1) == test_byte -Unfortunately, this doesn't work. **You cannot make a fixture that -opens a nursery or cancel scope, and then yields from inside the -nursery or cancel scope block.** Sorry 🙁. We're `working on it -`__. +And finally, remember, we need to write lots of tests, and we don't +want to have to copy-paste the server setup code every time. Let's +factor it out into a fixture:: -Instead, pytest-trio provides a built-in fixture called -``test_nursery``. This is a nursery that's created before each test, -and then automatically cancelled after the test finishes. Which is -exactly what we need – in fact it's even simpler than our first try, -because now we don't need to worry about cancelling the nursery -ourselves. + @pytest.fixture + async def echo_server_connection(nursery): + listeners = await nursery.start( + partial(trio.serve_tcp, echo_server_handler, port=0) + ) + echo_client = await open_stream_to_socket_listener(listeners[0]) + async with echo_client: + yield echo_client -So here's our complete, final version:: +And now in tests, all we have to do is request the ``echo_client`` +fixture, and we get a background server and a client stream connected +to it. So here's our complete, final version:: # Final version -- copy this! from functools import partial @@ -376,18 +384,18 @@ So here's our complete, final version:: # The fixture: @pytest.fixture - async def echo_server_connection(test_nursery): - listeners = await test_nursery.start( + async def echo_client(nursery): + listeners = await nursery.start( partial(trio.serve_tcp, echo_server_handler, port=0) ) - client = await open_stream_to_socket_listener(listeners[0]) - async with client: - yield client + echo_client = await open_stream_to_socket_listener(listeners[0]) + async with echo_client: + yield echo_client # A test using the fixture: - async def test_final(echo_server_connection): - for test_byte in [b"a", b"c", b"c"]: - await echo_server_connection.send_all(test_byte) - assert await echo_server_connection.receive_some(1) == test_byte + async def test_final(echo_client): + for test_byte in [b"a", b"b", b"c"]: + await echo_client.send_all(test_byte) + assert await echo_client.receive_some(1) == test_byte No race conditions, no hangs, simple, clean, and reusable. diff --git a/docs/source/reference.rst b/docs/source/reference.rst index 1021093..8ddd954 100644 --- a/docs/source/reference.rst +++ b/docs/source/reference.rst @@ -106,6 +106,42 @@ but Trio fixtures **must be test scoped**. Class, module, and session scope are not supported. +Concurrent setup/teardown +------------------------- + +If your test uses multiple fixtures, then for speed, pytest-trio will +try to run their setup and teardown code concurrently whenever this is +possible while respecting the fixture dependencies. + +Here's an example, where a test depends on ``fix_b`` and ``fix_c``, +and these both depend on ``fix_a``:: + + @trio_fixture + def fix_a(): + ... + + @trio_fixture + def fix_b(fix_a): + ... + + @trio_fixture + def fix_c(fix_a): + ... + + @pytest.mark.trio + async def test_example(fix_b, fix_c): + ... + +When running ``test_example``, pytest-trio will perform the following +sequence of actions: + +1. Set up ``fix_a`` +2. Set up ``fix_b`` and ``fix_c``, concurrently. +3. Run the test. +4. Tear down ``fix_b`` and ``fix_c``, concurrently. +5. Tear down ``fix_a``. + + Built-in fixtures ----------------- @@ -140,16 +176,34 @@ available. For example, you can call mock_clock.jump(10) assert trio.current_time() == 10 -.. data:: test_nursery - - A nursery created and managed by pytest-trio itself. When - pytest-trio runs a test, it performs these steps in this order: - - 1. Open the ``test_nursery`` - 2. Set up all Trio fixtures. - 3. Run the test. - 4. Tear down all Trio fixtures. - 5. Cancel the ``test_nursery``. +.. data:: nursery + + A nursery created and managed by pytest-trio itself, which + surrounds the test/fixture that requested it, and is automatically + cancelled after the test/fixture completes. Basically, these are + equivalent:: + + # Boring way + async def test_with_background_task(): + async with trio.open_nursery() as nursery: + try: + ... + finally: + nursery.cancel_scope.cancel() + + # Fancy way + async def test_with_background_task(nursery): + ... + + For a fixture, the cancellation always happens after the fixture + completes its teardown phase. (Or if it doesn't have a teardown + phase, then the cancellation happens after the teardown phase + *would* have happened.) + + This fixture is even more magical than most pytest fixtures, + because if it gets requested several times within the same test, + then it creates multiple nurseries, one for each fixture/test that + requested it. See :ref:`server-fixture-example` for an example of how this can be used. From 14570bef2b75572f4cab3eead4b3a085a16c5ce4 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Fri, 17 Aug 2018 04:28:49 -0700 Subject: [PATCH 050/185] rearrange background server quickstart to flow more smoothly --- docs/source/quickstart.rst | 199 ++++++++++++++++++++----------------- 1 file changed, 110 insertions(+), 89 deletions(-) diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 1c0682d..0c6f918 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -197,12 +197,12 @@ arbitrary data, and then send it back out again:: # Usage: await trio.serve_tcp(echo_server_handler, ...) -Since this is such complicated and sophisticated code, we want to -write lots of tests to make sure it's working correctly. Our basic -strategy for each test will be to start the echo server running in a -background task, and then in the test body we'll connect to our -server, send it some test data, and see how it responds. Here's a -first attempt:: +Now we need to test it, to make sure it's working correctly. In fact, +since this is such complicated and sophisticated code, we're going to +write lots of tests for it. And they'll all follow the same basic +pattern: we'll start the echo server running in a background task, +then connect to it, send it some test data, and see how it responds. +Here's a first attempt:: # Let's cross our fingers and hope no-one else is using this port... PORT = 14923 @@ -223,17 +223,76 @@ first attempt:: await echo_client.send_all(test_byte) assert await echo_client.receive_some(1) == test_byte -This will mostly work, but it has a few problems. The first one is -that there's a race condition: We spawn a background task to call -``serve_tcp``, and then immediately try to connect to that server. -Sometimes this will work fine. But it takes a little while for the -server to start up and be ready to accept connections – so other +This will mostly work, but it has a few problems. The most obvious one +is that when we run it, even if everything works perfectly, it will +hang at the end of the test – we never shut down the server, so the +nursery block will wait forever for it to exit. + +To avoid this, we should cancel the nursery at the end of the test: + +.. code-block:: python3 + :emphasize-lines: 7,20,21 + + # Let's cross our fingers and hope no-one else is using this port... + PORT = 14923 + + # Don't copy this -- we can do better + async def test_attempt_2(): + async with trio.open_nursery() as nursery: + try: + # Start server running in the background + nursery.start_soon( + partial(trio.serve_tcp, echo_server_handler, port=PORT) + ) + + # Connect to the server. + echo_client = await trio.open_tcp_stream("127.0.0.1", PORT) + # Send some test data, and check that it gets echoed back + async with echo_client: + for test_byte in [b"a", b"b", b"c"]: + await echo_client.send_all(test_byte) + assert await echo_client.receive_some(1) == test_byte + finally: + nursery.cancel_scope.cancel() + +In fact, this pattern is *so* common, that pytest-trio provides a +handy :data:`nursery` fixture to let you skip the boilerplate. Just +add ``nursery`` to your test function arguments, and pytest-trio will +open a nursery, pass it in to your function, and then cancel it for +you afterwards: + +.. code-block:: python3 + :emphasize-lines: 5 + + # Let's cross our fingers and hope no-one else is using this port... + PORT = 14923 + + # Don't copy this -- we can do better + async def test_attempt_3(nursery): + # Start server running in the background + nursery.start_soon( + partial(trio.serve_tcp, echo_server_handler, port=PORT) + ) + + # Connect to the server. + echo_client = await trio.open_tcp_stream("127.0.0.1", PORT) + # Send some test data, and check that it gets echoed back + async with echo_client: + for test_byte in [b"a", b"b", b"c"]: + await echo_client.send_all(test_byte) + assert await echo_client.receive_some(1) == test_byte + +Next problem: we have a race condition. We spawn a background task to +call ``serve_tcp``, and then immediately try to connect to that +server. Sometimes this will work fine. But it takes a little while for +the server to start up and be ready to accept connections – so other times, randomly, our connection attempt will happen too quickly, and error out. After all – ``nursery.start_soon`` only promises that the -task will be started *soon*, not that it's actually happened. +task will be started *soon*, not that it's actually happened. So this +test will be flaky, and flaky tests are the worst. -To solve this race condition, we should switch to using ``await -nursery.start(...)``. You can `read its docs for full details +Fortunately, Trio makes this easy to solve, by switching to using +``await nursery.start(...)``. You can `read its docs for full details `__, but basically the idea is that both ``nursery.start_soon(...)`` and ``await nursery.start(...)`` create background tasks, but only @@ -241,26 +300,28 @@ but basically the idea is that both ``nursery.start_soon(...)`` and requires some cooperation from the background task: it has to notify ``nursery.start`` when it's ready. Fortunately, :func:`trio.serve_tcp` already knows how to cooperate with ``nursery.start``, so we can -write:: +write: + +.. code-block:: python3 + :emphasize-lines: 6-10 # Let's cross our fingers and hope no-one else is using this port... PORT = 14923 # Don't copy this -- we can do better - async def test_attempt_2(): - async with trio.open_nursery() as nursery: - # Start server running in the background - # AND wait for it to finish starting up before continuing - await nursery.start( - partial(trio.serve_tcp, echo_server_handler, port=PORT) - ) + async def test_attempt_4(nursery): + # Start server running in the background + # AND wait for it to finish starting up before continuing + await nursery.start( + partial(trio.serve_tcp, echo_server_handler, port=PORT) + ) - # Connect to the server - echo_client = await trio.open_tcp_stream("127.0.0.1", PORT) - async with echo_client: - for test_byte in [b"a", b"b", b"c"]: - await echo_client.send_all(test_byte) - assert await echo_client.receive_some(1) == test_byte + # Connect to the server + echo_client = await trio.open_tcp_stream("127.0.0.1", PORT) + async with echo_client: + for test_byte in [b"a", b"b", b"c"]: + await echo_client.send_all(test_byte) + assert await echo_client.receive_some(1) == test_byte That solves our race condition. Next issue: hardcoding the port number like this is a bad idea, because port numbers are a machine-wide @@ -284,79 +345,39 @@ function called :func:`trio.testing.open_stream_to_socket_listener` that can take a :class:`~trio.SocketListener` and make a connection to it. -Putting it all together:: - - from trio.testing import open_stream_to_socket_listener - - # Don't copy this -- we can do better - async def test_attempt_3(): - async with trio.open_nursery() as nursery: - # Start server running in the background - # AND wait for it to finish starting up before continuing - # AND find out where it's actually listening - listeners = await nursery.start( - partial(trio.serve_tcp, echo_server_handler, port=0) - ) - - # Connect to the server. - # There might be multiple listeners (example: IPv4 and - # IPv6), but we don't care which one we connect to, so we - # just use the first. - echo_client = await open_stream_to_socket_listener(listeners[0]) - async with echo_client: - for test_byte in [b"a", b"b", b"c"]: - await echo_client.send_all(test_byte) - assert await echo_client.receive_some(1) == test_byte +Putting it all together: -Okay, this is getting closer... but if we try to run it, we'll find -that it just hangs instead of completing. What's going on? +.. code-block:: python3 + :emphasize-lines: 1,8,13-16 -The problem is that we need to shut down the server – otherwise it -will keep waiting for new connections forever, and the test will never -finish. We can fix this by cancelling everything in the nursery once -our test is finished:: + from trio.testing import open_stream_to_socket_listener # Don't copy this -- it finally works, but we can still do better! - async def test_attempt_4(): - async with trio.open_nursery() as nursery: - try: - listeners = await nursery.start( - partial(trio.serve_tcp, echo_server_handler, port=0) - ) - echo_client = await open_stream_to_socket_listener(listeners[0]) - async with echo_client: - for test_byte in [b"a", b"b", b"c"]: - await echo_client.send_all(test_byte) - assert await echo_client.receive_some(1) == test_byte - finally: - # Shut down the server now that we're done testing it - nursery.cancel_scope.cancel() - -Okay, finally this test works correctly. But that's a lot of -boilerplate. Can we slim it down? We can! - -First, pytest-trio provides a magical fixture that takes care of the -nursery management boilerplate: just request the :data:`nursery` fixture, -and you get a nursery object that's already set up, and will be -automatically cancelled when you're done:: - - # Don't copy this -- it finally works, but we can *still* do better! async def test_attempt_5(nursery): + # Start server running in the background + # AND wait for it to finish starting up before continuing + # AND find out where it's actually listening listeners = await nursery.start( partial(trio.serve_tcp, echo_server_handler, port=0) ) - echo_echo_client = await open_stream_to_socket_listener(listeners[0]) + + # Connect to the server. + # There might be multiple listeners (example: IPv4 and + # IPv6), but we don't care which one we connect to, so we + # just use the first. + echo_client = await open_stream_to_socket_listener(listeners[0]) async with echo_client: for test_byte in [b"a", b"b", b"c"]: - await echo_echo_client.send_all(test_byte) - assert await echo_echo_client.receive_some(1) == test_byte + await echo_client.send_all(test_byte) + assert await echo_client.receive_some(1) == test_byte -And finally, remember, we need to write lots of tests, and we don't -want to have to copy-paste the server setup code every time. Let's -factor it out into a fixture:: +Now, this works – but there's still a lot of boilerplate. Remember, we +need to write lots of tests for this server, and we don't want to have +to copy-paste all that stuff into every test. Let's factor out the +setup into a fixture:: @pytest.fixture - async def echo_server_connection(nursery): + async def echo_client(nursery): listeners = await nursery.start( partial(trio.serve_tcp, echo_server_handler, port=0) ) @@ -398,4 +419,4 @@ to it. So here's our complete, final version:: await echo_client.send_all(test_byte) assert await echo_client.receive_some(1) == test_byte -No race conditions, no hangs, simple, clean, and reusable. +No hangs, no race conditions, simple, clean, and reusable. From ef0cd267ea62188a8e475c66cb584e7a2addc02a Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Wed, 22 Aug 2018 02:30:33 -0700 Subject: [PATCH 051/185] Share contextvars across fixtures This is sort of a gross hack, but it's simple and works. If it causes problems, then this comment discusses some other options: https://github.com/python-trio/pytest-trio/pull/50#issuecomment-414648141 Also added tests for using trio-asyncio in fixtures (which was the motivating use case for sharing contextvars). --- pytest_trio/_tests/test_contextvars.py | 37 ++++++++++++++++++++ pytest_trio/_tests/test_trio_asyncio.py | 43 +++++++++++++++++++++++ pytest_trio/plugin.py | 45 ++++++++++++++++++------- test-requirements.txt | 1 + 4 files changed, 113 insertions(+), 13 deletions(-) create mode 100644 pytest_trio/_tests/test_contextvars.py create mode 100644 pytest_trio/_tests/test_trio_asyncio.py diff --git a/pytest_trio/_tests/test_contextvars.py b/pytest_trio/_tests/test_contextvars.py new file mode 100644 index 0000000..33bf147 --- /dev/null +++ b/pytest_trio/_tests/test_contextvars.py @@ -0,0 +1,37 @@ +import pytest +from pytest_trio import trio_fixture + +import contextvars + +cv = contextvars.ContextVar("cv", default=None) + + +@trio_fixture +def cv_checker(): + assert cv.get() is None + yield + assert cv.get() is None + + +@trio_fixture +def cv_setter(cv_checker): + assert cv.get() is None + token = cv.set("cv_setter") + yield + assert cv.get() == "cv_setter2" + cv.reset(token) + assert cv.get() is None + + +@trio_fixture +def cv_setter2(cv_setter): + assert cv.get() == "cv_setter" + # Intentionally leak, so can check that this is visible back in cv_setter + cv.set("cv_setter2") + yield + assert cv.get() == "cv_setter2" + + +@pytest.mark.trio +async def test_contextvars(cv_setter2): + assert cv.get() == "cv_setter2" diff --git a/pytest_trio/_tests/test_trio_asyncio.py b/pytest_trio/_tests/test_trio_asyncio.py new file mode 100644 index 0000000..a096a79 --- /dev/null +++ b/pytest_trio/_tests/test_trio_asyncio.py @@ -0,0 +1,43 @@ +import pytest +import trio_asyncio +import asyncio + + +@trio_asyncio.trio2aio +async def work_in_asyncio(): + await asyncio.sleep(0) + + +@pytest.fixture() +async def asyncio_loop(): + async with trio_asyncio.open_loop() as loop: + yield loop + + +@pytest.fixture() +async def asyncio_fixture_with_fixtured_loop(asyncio_loop): + await work_in_asyncio() + yield 42 + + +@pytest.fixture() +async def asyncio_fixture_own_loop(): + async with trio_asyncio.open_loop(): + await work_in_asyncio() + yield 42 + + +@pytest.mark.trio +async def test_no_fixture(): + async with trio_asyncio.open_loop(): + await work_in_asyncio() + + +@pytest.mark.trio +async def test_half_fixtured_asyncpg_conn(asyncio_fixture_own_loop): + await work_in_asyncio() + + +@pytest.mark.trio +async def test_fixtured_asyncpg_conn(asyncio_fixture_with_fixtured_loop): + await work_in_asyncio() diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index fa78c89..498593b 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -3,6 +3,7 @@ from traceback import format_exception from collections.abc import Coroutine, Generator from inspect import iscoroutinefunction, isgeneratorfunction +import contextvars import pytest import trio from trio.testing import MockClock, trio_test @@ -110,6 +111,8 @@ def pytest_exception_interact(node, call, report): # follow the normal teardown sequence, and so on – but since the test is # cancelled, the teardown sequence should start immediately. +canary = contextvars.ContextVar("pytest-trio canary") + class TrioTestContext: def __init__(self): @@ -146,7 +149,7 @@ def __init__(self, name, func, pytest_kwargs, is_test=False): self.fixture_value = None # This event notifies downstream users that we're done setting up. # Invariant: if this is set, then either fixture_value is usable *or* - # ctx.crashed is True. + # test_ctx.crashed is True. self.setup_done = trio.Event() # Downstream users *modify* this value, by adding their _teardown_done # events to it, so we know who we need to wait for before tearing @@ -166,7 +169,7 @@ def register_and_collect_dependencies(self): @asynccontextmanager @async_generator - async def _fixture_manager(self, ctx): + async def _fixture_manager(self, test_ctx): __tracebackhide__ = True try: async with trio.open_nursery() as nursery_fixture: @@ -175,18 +178,29 @@ async def _fixture_manager(self, ctx): finally: nursery_fixture.cancel_scope.cancel() except BaseException as exc: - ctx.crash(exc) + test_ctx.crash(exc) finally: self.setup_done.set() self._teardown_done.set() - async def run(self, ctx): + async def run(self, test_ctx, contextvars_ctx): __tracebackhide__ = True + # This is a gross hack. I guess Trio should provide a context= + # argument to start_soon/start? + task = trio.hazmat.current_task() + assert canary not in task.context + task.context = contextvars_ctx + # Force a yield so we pick up the new context + await trio.sleep(0) + # Check that it worked, since technically trio doesn't *guarantee* + # that sleep(0) will actually yield. + assert canary.get() == "in correct context" + # This 'with' block handles the nursery fixture lifetime, the # teardone_done event, and crashing the context if there's an # unhandled exception. - async with self._fixture_manager(ctx) as nursery_fixture: + async with self._fixture_manager(test_ctx) as nursery_fixture: # Resolve our kwargs resolved_kwargs = {} for name, value in self._pytest_kwargs.items(): @@ -201,7 +215,7 @@ async def run(self, ctx): # If something's already crashed before we're ready to start, then # there's no point in even setting up. - if ctx.crashed: + if test_ctx.crashed: return # Run actual fixture setup step @@ -212,8 +226,8 @@ async def run(self, ctx): assert not self.user_done_events func_value = None with trio.open_cancel_scope() as cancel_scope: - ctx.test_cancel_scope = cancel_scope - assert not ctx.crashed + test_ctx.test_cancel_scope = cancel_scope + assert not test_ctx.crashed await self._func(**resolved_kwargs) else: func_value = self._func(**resolved_kwargs) @@ -255,7 +269,7 @@ async def run(self, ctx): await event.wait() except BaseException as exc: assert isinstance(exc, trio.Cancelled) - ctx.crash(None) + test_ctx.crash(None) with trio.open_cancel_scope(shield=True): for event in self.user_done_events: await event.wait() @@ -294,7 +308,7 @@ def _trio_test_runner_factory(item, testfunc=None): async def _bootstrap_fixtures_and_run_test(**kwargs): __tracebackhide__ = True - ctx = TrioTestContext() + test_ctx = TrioTestContext() test = TrioFixture( "".format(testfunc.__name__), testfunc, @@ -302,12 +316,17 @@ async def _bootstrap_fixtures_and_run_test(**kwargs): is_test=True ) + contextvars_ctx = contextvars.copy_context() + contextvars_ctx.run(canary.set, "in correct context") + async with trio.open_nursery() as nursery: for fixture in test.register_and_collect_dependencies(): - nursery.start_soon(fixture.run, ctx, name=fixture.name) + nursery.start_soon( + fixture.run, test_ctx, contextvars_ctx, name=fixture.name + ) - if ctx.error_list: - raise trio.MultiError(ctx.error_list) + if test_ctx.error_list: + raise trio.MultiError(test_ctx.error_list) _bootstrap_fixtures_and_run_test._trio_test_runner_wrapped = True return _bootstrap_fixtures_and_run_test diff --git a/test-requirements.txt b/test-requirements.txt index b2be0f9..d2ac8c8 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,4 @@ pytest !=3.7.0, !=3.7.1 # https://github.com/python-trio/pytest-trio/pull/50#issuecomment-413124393 pytest-cov hypothesis>=3.64 +trio-asyncio From 75befd350f354a34601a9f81e58435ff522340dd Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Wed, 22 Aug 2018 02:46:42 -0700 Subject: [PATCH 052/185] Add docs for ContextVar support --- docs/source/index.rst | 15 +++++++++++---- docs/source/reference.rst | 26 ++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 9af200c..fd29f82 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -21,15 +21,22 @@ and async I/O in Python. Features include: * Write your own async fixtures: set up an async database connection or start a server inside a fixture, and then use it in your tests. - If you have multiple async fixtures, pytest-trio will even do - setup/teardown concurrently whenever possible. + +* If you have multiple async fixtures, pytest-trio will even do + setup/teardown concurrently whenever possible. (Though honestly, + we're not sure whether this is a good idea or not and might remove + it in the future. If it makes your tests harder to debug, or + conversely provides you with big speedups, `please let us know + `__.) * Integration with the fabulous `Hypothesis `__ library, so your async tests can use property-based testing: just use ``@given`` like you're used to. -* Supports testing projects that use Trio exclusively, and also - projects that support multiple async libraries. +* Support for testing projects that use Trio exclusively and want to + use pytest-trio everywhere, and also for testing projects that + support multiple async libraries and only want to enable + pytest-trio's features for a subset of their test suite. Vital statistics diff --git a/docs/source/reference.rst b/docs/source/reference.rst index 8ddd954..ccdae6a 100644 --- a/docs/source/reference.rst +++ b/docs/source/reference.rst @@ -141,6 +141,32 @@ sequence of actions: 4. Tear down ``fix_b`` and ``fix_c``, concurrently. 5. Tear down ``fix_a``. +We're `seeking feedback +`__ on whether +this feature's benefits outweigh its negatives. + + +Handling of ContextVars +----------------------- + +The :mod:`contextvars` module provides a way to create task-local +variables called :class:`~contextvars.ContextVars`. Normally, in Trio, +each task gets its own :class:`~contextvars.Context`. But pytest-trio +overrides this, and for each test it uses a single +:class:`~contextvars.Context` which is shared by all fixtures and the +test function itself. + +The benefit of this is that you can set +:class:`~contextvars.ContextVars` inside a fixture, and your settings +will be visible in dependent fixtures and the test itself. + +The downside is that if two fixtures are run concurrently (see +previous section), and both mutate the same +:class:`~contextvars.ContextVars`, then there will be a race condition +and the the final value will be unpredictable. If you make one fixture +depend on the other, then this will force an ordering and make the +final value predictable again. + Built-in fixtures ----------------- From 2e5f6f7426f22b65685c6e630328c8a6cd1e732d Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Wed, 22 Aug 2018 02:48:22 -0700 Subject: [PATCH 053/185] py35 compatibility --- pytest_trio/_tests/test_trio_asyncio.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pytest_trio/_tests/test_trio_asyncio.py b/pytest_trio/_tests/test_trio_asyncio.py index a096a79..7acc2eb 100644 --- a/pytest_trio/_tests/test_trio_asyncio.py +++ b/pytest_trio/_tests/test_trio_asyncio.py @@ -1,7 +1,7 @@ import pytest import trio_asyncio import asyncio - +from async_generator import async_generator, yield_ @trio_asyncio.trio2aio async def work_in_asyncio(): @@ -9,22 +9,25 @@ async def work_in_asyncio(): @pytest.fixture() +@async_generator async def asyncio_loop(): async with trio_asyncio.open_loop() as loop: - yield loop + await yield_(loop) @pytest.fixture() +@async_generator async def asyncio_fixture_with_fixtured_loop(asyncio_loop): await work_in_asyncio() - yield 42 + await yield_() @pytest.fixture() +@async_generator async def asyncio_fixture_own_loop(): async with trio_asyncio.open_loop(): await work_in_asyncio() - yield 42 + await yield_() @pytest.mark.trio From b0559c1d492a818856eea5c4bcec88d0f24e9e04 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Wed, 22 Aug 2018 02:55:28 -0700 Subject: [PATCH 054/185] Skip trio-asyncio tests on py35 When importing trio-asyncio v0.8.0 on py35, we get a crash at import time: .../python3.5/site-packages/trio_asyncio/loop.py:171: in _orig_run_get = _aio_event._get_running_loop E AttributeError: module 'asyncio.events' has no attribute '_get_running_loop' See: https://travis-ci.org/python-trio/pytest-trio/jobs/419092404 So let's skip testing this for now... --- pytest_trio/_tests/test_trio_asyncio.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pytest_trio/_tests/test_trio_asyncio.py b/pytest_trio/_tests/test_trio_asyncio.py index 7acc2eb..a38ddda 100644 --- a/pytest_trio/_tests/test_trio_asyncio.py +++ b/pytest_trio/_tests/test_trio_asyncio.py @@ -1,8 +1,16 @@ import pytest -import trio_asyncio +import sys import asyncio from async_generator import async_generator, yield_ +if sys.version_info < (3, 6): + pytestmark = pytest.mark.skip( + reason="trio-asyncio doesn't seem to work on 3.5" + ) +else: + import trio_asyncio + + @trio_asyncio.trio2aio async def work_in_asyncio(): await asyncio.sleep(0) From d63783c3870b465ea4d7a951385aac7b93b210df Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Wed, 22 Aug 2018 03:04:14 -0700 Subject: [PATCH 055/185] fix xrefs --- docs/source/reference.rst | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/source/reference.rst b/docs/source/reference.rst index ccdae6a..541b7e7 100644 --- a/docs/source/reference.rst +++ b/docs/source/reference.rst @@ -149,20 +149,22 @@ this feature's benefits outweigh its negatives. Handling of ContextVars ----------------------- -The :mod:`contextvars` module provides a way to create task-local -variables called :class:`~contextvars.ContextVars`. Normally, in Trio, -each task gets its own :class:`~contextvars.Context`. But pytest-trio +The :mod:`contextvars` module lets you create +:class:`~contextvars.ContextVar` objects to represent task-local +variables. Normally, in Trio, each task gets its own +:class:`~contextvars.Context`, so that :class:`~ContextVar` mutations +are only visible to the task that performs them. But pytest-trio overrides this, and for each test it uses a single :class:`~contextvars.Context` which is shared by all fixtures and the test function itself. The benefit of this is that you can set -:class:`~contextvars.ContextVars` inside a fixture, and your settings -will be visible in dependent fixtures and the test itself. +:class:`~contextvars.ContextVar` values inside a fixture, and your +settings will be visible in dependent fixtures and the test itself. The downside is that if two fixtures are run concurrently (see previous section), and both mutate the same -:class:`~contextvars.ContextVars`, then there will be a race condition +:class:`~contextvars.ContextVar`, then there will be a race condition and the the final value will be unpredictable. If you make one fixture depend on the other, then this will force an ordering and make the final value predictable again. From b89b34d993ecbc16c0ec8c3f5f61d0edd5e11bb8 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Wed, 22 Aug 2018 03:06:26 -0700 Subject: [PATCH 056/185] this time for sure... --- docs/source/reference.rst | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/source/reference.rst b/docs/source/reference.rst index 541b7e7..6a56539 100644 --- a/docs/source/reference.rst +++ b/docs/source/reference.rst @@ -152,15 +152,19 @@ Handling of ContextVars The :mod:`contextvars` module lets you create :class:`~contextvars.ContextVar` objects to represent task-local variables. Normally, in Trio, each task gets its own -:class:`~contextvars.Context`, so that :class:`~ContextVar` mutations -are only visible to the task that performs them. But pytest-trio -overrides this, and for each test it uses a single -:class:`~contextvars.Context` which is shared by all fixtures and the -test function itself. +:class:`~contextvars.Context`, so that changes to +:class:`~contextvars.ContextVar` objects are only visible inside the +task that performs them. But pytest-trio overrides this, and for each +test it uses a single :class:`~contextvars.Context` which is shared by +all fixtures and the test function itself. The benefit of this is that you can set :class:`~contextvars.ContextVar` values inside a fixture, and your settings will be visible in dependent fixtures and the test itself. +For example, `trio-asyncio `__ +uses a :class:`~contextvars.ContextVar` to hold the current asyncio +loop object, so this lets you open a loop inside a fixture and then +use it inside other fixtures or the test itself. The downside is that if two fixtures are run concurrently (see previous section), and both mutate the same From 98ba48be4fd8703e401d5ae02b1479de7cc11862 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Wed, 22 Aug 2018 03:08:06 -0700 Subject: [PATCH 057/185] THIS TIME FOR SURE --- pytest_trio/_tests/test_trio_asyncio.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pytest_trio/_tests/test_trio_asyncio.py b/pytest_trio/_tests/test_trio_asyncio.py index a38ddda..d899bb8 100644 --- a/pytest_trio/_tests/test_trio_asyncio.py +++ b/pytest_trio/_tests/test_trio_asyncio.py @@ -11,9 +11,8 @@ import trio_asyncio -@trio_asyncio.trio2aio -async def work_in_asyncio(): - await asyncio.sleep(0) +async def use_run_asyncio(): + await trio_asyncio.run_asyncio(asyncio.sleep, 0) @pytest.fixture() @@ -26,7 +25,7 @@ async def asyncio_loop(): @pytest.fixture() @async_generator async def asyncio_fixture_with_fixtured_loop(asyncio_loop): - await work_in_asyncio() + await use_run_asyncio() await yield_() @@ -34,21 +33,21 @@ async def asyncio_fixture_with_fixtured_loop(asyncio_loop): @async_generator async def asyncio_fixture_own_loop(): async with trio_asyncio.open_loop(): - await work_in_asyncio() + await use_run_asyncio() await yield_() @pytest.mark.trio async def test_no_fixture(): async with trio_asyncio.open_loop(): - await work_in_asyncio() + await use_run_asyncio() @pytest.mark.trio async def test_half_fixtured_asyncpg_conn(asyncio_fixture_own_loop): - await work_in_asyncio() + await use_run_asyncio() @pytest.mark.trio async def test_fixtured_asyncpg_conn(asyncio_fixture_with_fixtured_loop): - await work_in_asyncio() + await use_run_asyncio() From 8ed00927a3b8d3b2a3beb612d61597f9fc0dca22 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Fri, 24 Aug 2018 05:06:27 -0700 Subject: [PATCH 058/185] Update README also --- README.rst | 44 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index fe1b031..e919304 100644 --- a/README.rst +++ b/README.rst @@ -1,17 +1,49 @@ pytest-trio =========== +.. image:: https://img.shields.io/badge/chat-join%20now-blue.svg + :target: https://gitter.im/python-trio/general + :alt: Join chatroom + +.. image:: https://img.shields.io/badge/docs-read%20now-blue.svg + :target: https://pytest-trio.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + +.. image:: https://img.shields.io/pypi/v/pytest-trio.svg + :target: https://pypi.org/project/pytest-trio + :alt: Latest PyPi version + .. image:: https://travis-ci.org/python-trio/pytest-trio.svg?branch=master - :target: https://travis-ci.org/python-trio/pytest-trio + :target: https://travis-ci.org/python-trio/pytest-trio + :alt: Automated test status (Linux and MacOS) .. image:: https://ci.appveyor.com/api/projects/status/aq0pklx7hanx031x?svg=true - :target: https://ci.appveyor.com/project/touilleMan/pytest-trio + :target: https://ci.appveyor.com/project/touilleMan/pytest-trio + :alt: Automated test status (Windows) .. image:: https://codecov.io/gh/python-trio/pytest-trio/branch/master/graph/badge.svg - :target: https://codecov.io/gh/python-trio/pytest-trio + :target: https://codecov.io/gh/python-trio/pytest-trio + :alt: Test coverage + +This is a pytest plugin to help you test projects that use `Trio +`__, a friendly library for concurrency +and async I/O in Python. For an overview of features, see our `manual +`__, or jump straight to the +`quickstart guide +`__. + + +Vital statistics +---------------- + +**Documentation:** https://pytest-trio.readthedocs.io -Welcome to `pytest-trio `__! +**Bug tracker and source code:** +https://github.com/python-trio/pytest-trio -Pytest plugin for trio +**License:** MIT or Apache 2, your choice. -License: Your choice of MIT or Apache License 2.0 +**Code of conduct:** Contributors are requested to follow our `code of +conduct +`__ +in all project spaces. From 038e58960e2c53e0be2edb905c600065df751613 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Sun, 26 Aug 2018 22:07:28 -0700 Subject: [PATCH 059/185] Bump version to 0.5.0 --- docs/source/history.rst | 4 ++-- pytest_trio/_version.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/history.rst b/docs/source/history.rst index 2f8fce7..84bfc0e 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -5,12 +5,12 @@ Release history .. towncrier release notes start -pytest-trio 0.5.0 (????-??-??) +pytest-trio 0.5.0 (2018-08-26) ------------------------------ This is a major release, including a rewrite of large portions of the internals. We believe it should be backwards compatible with existing -projects, but major new features include: +projects. Major new features include: * "trio mode": no more writing ``@pytest.mark.trio`` everywhere! * it's now safe to use nurseries inside fixtures (`#55 diff --git a/pytest_trio/_version.py b/pytest_trio/_version.py index 3c33369..f637025 100644 --- a/pytest_trio/_version.py +++ b/pytest_trio/_version.py @@ -1,3 +1,3 @@ # This file is imported from __init__.py and exec'd from setup.py -__version__ = "0.4.2" +__version__ = "0.5.0" From 8e8f9fd94f10850a7047f1a7116ce324ecbaf277 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Sun, 26 Aug 2018 22:09:06 -0700 Subject: [PATCH 060/185] Bump version to 0.5.0+dev --- pytest_trio/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_trio/_version.py b/pytest_trio/_version.py index f637025..0a95219 100644 --- a/pytest_trio/_version.py +++ b/pytest_trio/_version.py @@ -1,3 +1,3 @@ # This file is imported from __init__.py and exec'd from setup.py -__version__ = "0.5.0" +__version__ = "0.5.0+dev" From 874fe40a3584c1537330fbc11c532d91886d78f3 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Sun, 26 Aug 2018 22:15:32 -0700 Subject: [PATCH 061/185] Don't try to install trio-asyncio on Pythons where it doesn't work --- pytest_trio/_tests/test_trio_asyncio.py | 8 +++----- test-requirements.txt | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pytest_trio/_tests/test_trio_asyncio.py b/pytest_trio/_tests/test_trio_asyncio.py index d899bb8..4bdb3f7 100644 --- a/pytest_trio/_tests/test_trio_asyncio.py +++ b/pytest_trio/_tests/test_trio_asyncio.py @@ -3,12 +3,10 @@ import asyncio from async_generator import async_generator, yield_ -if sys.version_info < (3, 6): - pytestmark = pytest.mark.skip( - reason="trio-asyncio doesn't seem to work on 3.5" - ) -else: +try: import trio_asyncio +except ImportError: + pytestmark = pytest.mark.skip(reason="trio-asyncio not available") async def use_run_asyncio(): diff --git a/test-requirements.txt b/test-requirements.txt index d2ac8c8..af55c2c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,4 @@ pytest !=3.7.0, !=3.7.1 # https://github.com/python-trio/pytest-trio/pull/50#issuecomment-413124393 pytest-cov hypothesis>=3.64 -trio-asyncio +trio-asyncio; python_version >= "3.5.3" From 398797a9e23409ffb3f7a853ca302e9d70cef704 Mon Sep 17 00:00:00 2001 From: Matthias Urlichs Date: Mon, 27 Aug 2018 08:59:01 +0200 Subject: [PATCH 062/185] Itsology Fix a couple of questionable (and one completely wrong) instances of "it's". --- docs/source/quickstart.rst | 6 +++--- pytest_trio/plugin.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 0c6f918..e5eac35 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -137,7 +137,7 @@ But now we're using virtual time, so the call to ``await trio.sleep(1)`` takes *exactly* 1 virtual second, and the ``==`` test will pass every time. Before, we had to wait around for the test to complete; now, it completes essentially instantaneously. (Try it!) -And, while here our example is super simple, it's integration with +And, while here our example is super simple, its integration with Trio's core scheduling logic allows this to work for arbitrarily complex programs (as long as they aren't interacting with the outside world). @@ -288,7 +288,7 @@ server. Sometimes this will work fine. But it takes a little while for the server to start up and be ready to accept connections – so other times, randomly, our connection attempt will happen too quickly, and error out. After all – ``nursery.start_soon`` only promises that the -task will be started *soon*, not that it's actually happened. So this +task will be started *soon*, not that it has actually happened. So this test will be flaky, and flaky tests are the worst. Fortunately, Trio makes this easy to solve, by switching to using @@ -338,7 +338,7 @@ Well, there's no way to predict the port ahead of time. But after :func:`~trio.serve_tcp` has opened a port, it can check and see what it got. So we need some way to pass this data back out of :func:`~trio.serve_tcp`. Fortunately, ``nursery.start`` handles this -too: it lets the task pass out a piece of data after it's started. And +too: it lets the task pass out a piece of data after it has started. And it just so happens that what :func:`~trio.serve_tcp` passes out is a list of :class:`~trio.SocketListener` objects. And there's a handy function called :func:`trio.testing.open_stream_to_socket_listener` diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 498593b..77cbb13 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -92,7 +92,7 @@ def pytest_exception_interact(node, call, report): # # You should think of fixtures as a dependency graph: each fixtures *uses* # zero or more other fixtures, and is *used by* zero or more other fixtures. A -# fixture should be setup before any of the fixtures it's used by are setup, +# fixture should be setup before any of the fixtures it is used by are setup, # and it should be torn down as soon as all of the fixtures that use it are # torn down. And at root of this dependency graph, we have the test itself, # which is just like a fixture except that instead of having a separate setup From d2c57e8990519f019394c65f343075b2ddb25421 Mon Sep 17 00:00:00 2001 From: Matthias Urlichs Date: Mon, 27 Aug 2018 09:07:34 +0200 Subject: [PATCH 063/185] =?UTF-8?q?"where=20a=20you=20use"=20=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/reference.rst b/docs/source/reference.rst index 6a56539..74f0b41 100644 --- a/docs/source/reference.rst +++ b/docs/source/reference.rst @@ -19,7 +19,7 @@ When Trio mode is enabled, two extra things happen: have to do it yourself. * Async fixtures using ``@pytest.fixture`` automatically get converted to Trio fixtures. (The main effect of this is that it helps you - catch mistakes where a you use an async fixture with a non-async + catch mistakes like using an async fixture with a non-async test.) There are two ways to enable Trio mode. From b904494b16c8ded65d30be215c3fb3e7a5610c09 Mon Sep 17 00:00:00 2001 From: Matthias Urlichs Date: Tue, 28 Aug 2018 14:12:18 +0200 Subject: [PATCH 064/185] Re-word somewhat-awkward sentence --- pytest_trio/plugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 77cbb13..3ababf8 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -91,10 +91,10 @@ def pytest_exception_interact(node, call, report): # and do something sensible with it. # # You should think of fixtures as a dependency graph: each fixtures *uses* -# zero or more other fixtures, and is *used by* zero or more other fixtures. A -# fixture should be setup before any of the fixtures it is used by are setup, -# and it should be torn down as soon as all of the fixtures that use it are -# torn down. And at root of this dependency graph, we have the test itself, +# zero or more other fixtures, and is *used by* zero or more other fixtures. +# A fixture should be setup before any of its dependees are setup, and torn +# down once all of its dependees have terminated. +# At the root of this dependency graph, we have the test itself, # which is just like a fixture except that instead of having a separate setup # and teardown phase, it runs straight through. # From 91346e6ebafbd6f4abacc6143b632bf07448c831 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Wed, 26 Sep 2018 01:14:59 -0700 Subject: [PATCH 065/185] Fix compatibility with pytest 3.8.1 Apparently for a test *method*, item.obj and item.function are different: item.obj is the bound method, and item.function is the underlying unbound method. At least in pytest 3.8.1. We want to wrap the bound method, not the unbound method, so we should wrap item.obj, not item.function. --- newsfragments/64.bugfix.rst | 2 ++ pytest_trio/plugin.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 newsfragments/64.bugfix.rst diff --git a/newsfragments/64.bugfix.rst b/newsfragments/64.bugfix.rst new file mode 100644 index 0000000..67c7a74 --- /dev/null +++ b/newsfragments/64.bugfix.rst @@ -0,0 +1,2 @@ +The pytest 3.8.1 release broke pytest-trio's handling of trio tests +defined as class methods. We fixed it again. diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 3ababf8..b567cf9 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -292,7 +292,7 @@ async def run(self, test_ctx, contextvars_ctx): def _trio_test_runner_factory(item, testfunc=None): - testfunc = testfunc or item.function + testfunc = testfunc or item.obj if getattr(testfunc, '_trio_test_runner_wrapped', False): # We have already wrapped this, perhaps because we combined Hypothesis From ffbceceffc2a59d7283fcddc622fe4b2224dff9e Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Wed, 26 Sep 2018 01:35:22 -0700 Subject: [PATCH 066/185] Hack to let us keep merging stuff while gh-66 is being resolved --- pytest_trio/_tests/conftest.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pytest_trio/_tests/conftest.py b/pytest_trio/_tests/conftest.py index c6481d5..38c03f7 100644 --- a/pytest_trio/_tests/conftest.py +++ b/pytest_trio/_tests/conftest.py @@ -1 +1,10 @@ +# Temporary hack while waiting for an answer here: +# https://github.com/pytest-dev/pytest/issues/4039 +import pytest +import warnings +warnings.filterwarnings( + "default", category=pytest.RemovedInPytest4Warning, + message=".*non-top-level conftest.*", +) + pytest_plugins = ["pytester"] From 36f087ff679ae5980d22c620ddbd155b6151fd1a Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Wed, 26 Sep 2018 01:46:20 -0700 Subject: [PATCH 067/185] Run yapf --- pytest_trio/_tests/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pytest_trio/_tests/conftest.py b/pytest_trio/_tests/conftest.py index 38c03f7..7e1ccd9 100644 --- a/pytest_trio/_tests/conftest.py +++ b/pytest_trio/_tests/conftest.py @@ -3,7 +3,8 @@ import pytest import warnings warnings.filterwarnings( - "default", category=pytest.RemovedInPytest4Warning, + "default", + category=pytest.RemovedInPytest4Warning, message=".*non-top-level conftest.*", ) From aa79c6ee35ab9a17f33c75db107c3ff397b83586 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Wed, 26 Sep 2018 01:47:46 -0700 Subject: [PATCH 068/185] Avoid using deprecated run_asyncio I guess this will break in the future as we work through https://github.com/python-trio/trio-asyncio/issues/42 but right now I don't care I just want these freaking tests to pass. --- pytest_trio/_tests/test_trio_asyncio.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pytest_trio/_tests/test_trio_asyncio.py b/pytest_trio/_tests/test_trio_asyncio.py index 4bdb3f7..8a3d9d0 100644 --- a/pytest_trio/_tests/test_trio_asyncio.py +++ b/pytest_trio/_tests/test_trio_asyncio.py @@ -9,8 +9,8 @@ pytestmark = pytest.mark.skip(reason="trio-asyncio not available") -async def use_run_asyncio(): - await trio_asyncio.run_asyncio(asyncio.sleep, 0) +async def use_asyncio(): + await trio_asyncio.aio_as_trio(asyncio.sleep)(0) @pytest.fixture() @@ -23,7 +23,7 @@ async def asyncio_loop(): @pytest.fixture() @async_generator async def asyncio_fixture_with_fixtured_loop(asyncio_loop): - await use_run_asyncio() + await use_asyncio() await yield_() @@ -31,21 +31,21 @@ async def asyncio_fixture_with_fixtured_loop(asyncio_loop): @async_generator async def asyncio_fixture_own_loop(): async with trio_asyncio.open_loop(): - await use_run_asyncio() + await use_asyncio() await yield_() @pytest.mark.trio async def test_no_fixture(): async with trio_asyncio.open_loop(): - await use_run_asyncio() + await use_asyncio() @pytest.mark.trio async def test_half_fixtured_asyncpg_conn(asyncio_fixture_own_loop): - await use_run_asyncio() + await use_asyncio() @pytest.mark.trio async def test_fixtured_asyncpg_conn(asyncio_fixture_with_fixtured_loop): - await use_run_asyncio() + await use_asyncio() From 3a175b33bf7e1f6e7c7b37006e5e8ebef2f3063e Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Fri, 28 Sep 2018 10:21:52 +0200 Subject: [PATCH 069/185] Bump version to 0.5.1 --- docs/source/history.rst | 10 ++++++++++ newsfragments/64.bugfix.rst | 2 -- pytest_trio/_version.py | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 newsfragments/64.bugfix.rst diff --git a/docs/source/history.rst b/docs/source/history.rst index 84bfc0e..f3e5086 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -5,6 +5,16 @@ Release history .. towncrier release notes start +pytest-trio 0.5.1 (2018-09-28) +------------------------------ + +Bugfixes +~~~~~~~~ + +- The pytest 3.8.1 release broke pytest-trio's handling of trio tests + defined as class methods. We fixed it again. (`#64 `__) + + pytest-trio 0.5.0 (2018-08-26) ------------------------------ diff --git a/newsfragments/64.bugfix.rst b/newsfragments/64.bugfix.rst deleted file mode 100644 index 67c7a74..0000000 --- a/newsfragments/64.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -The pytest 3.8.1 release broke pytest-trio's handling of trio tests -defined as class methods. We fixed it again. diff --git a/pytest_trio/_version.py b/pytest_trio/_version.py index 0a95219..f09277d 100644 --- a/pytest_trio/_version.py +++ b/pytest_trio/_version.py @@ -1,3 +1,3 @@ # This file is imported from __init__.py and exec'd from setup.py -__version__ = "0.5.0+dev" +__version__ = "0.5.1" From e7fd67a38a02d8f79b4ef608568301475eb0cab7 Mon Sep 17 00:00:00 2001 From: Zac-HD Date: Wed, 31 Oct 2018 21:54:53 +1100 Subject: [PATCH 070/185] Add new Hypothesis trove classifier --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 6b25497..2a7e945 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ "Programming Language :: Python :: Implementation :: PyPy", "Topic :: System :: Networking", "Topic :: Software Development :: Testing", + "Framework :: Hypothesis", "Framework :: Pytest", "Framework :: Trio", ], From bae1fbc7056de3902eda367f1193227e0825f6fc Mon Sep 17 00:00:00 2001 From: Vincent Michel Date: Fri, 11 Jan 2019 18:28:01 +0100 Subject: [PATCH 071/185] Move pytest options to pytest inifile --- ci/travis.sh | 2 +- pytest.ini | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 pytest.ini diff --git a/ci/travis.sh b/ci/travis.sh index 22230d3..6cee6a7 100755 --- a/ci/travis.sh +++ b/ci/travis.sh @@ -92,7 +92,7 @@ else mkdir empty cd empty - coverage run --rcfile=../.coveragerc -m pytest -W error -ra -v --pyargs pytest_trio --verbose + pytest bash <(curl -s https://codecov.io/bash) fi diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..e7e9beb --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = -W error -ra -v --pyargs pytest_trio --verbose --cov From 3775f989474ed7ffeab597779f7981e2634c0257 Mon Sep 17 00:00:00 2001 From: Zac-HD Date: Sun, 6 Jan 2019 14:28:42 +1100 Subject: [PATCH 072/185] Deterministic scheduling via Hypothesis --- newsfragments/73.feature.rst | 3 +++ pytest_trio/plugin.py | 13 ++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 newsfragments/73.feature.rst diff --git a/newsfragments/73.feature.rst b/newsfragments/73.feature.rst new file mode 100644 index 0000000..9993cdf --- /dev/null +++ b/newsfragments/73.feature.rst @@ -0,0 +1,3 @@ +pytest-trio now makes the Trio scheduler deterministic while running +inside a Hypothesis test. Hopefully you won't see any change, but if +you had scheduler-dependent bugs Hypothesis will be more effective now. diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index b567cf9..9d86159 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -22,6 +22,16 @@ # Ordered dict (and **kwargs) not available with Python<3.6 ORDERED_DICTS = False +try: + from hypothesis import register_random +except ImportError: # pragma: no cover + pass +else: + # On recent versions of Hypothesis, make the Trio scheduler deterministic + # even though it uses a module-scoped Random instance. This works + # regardless of whether or not the random_module strategy is used. + register_random(trio._core._run._r) + def pytest_addoption(parser): parser.addini( @@ -345,7 +355,8 @@ def pytest_runtest_call(item): item.obj.hypothesis.inner_test = _trio_test_runner_factory( item, item.obj.hypothesis.inner_test ) - elif getattr(item.obj, 'is_hypothesis_test', False): + elif getattr(item.obj, 'is_hypothesis_test', + False): # pragma: no cover pytest.fail( 'test function `%r` is using Hypothesis, but pytest-trio ' 'only works with Hypothesis 3.64.0 or later.' % item From 5e12d310b8f33c3b150cb16926e9379a03baed11 Mon Sep 17 00:00:00 2001 From: Zac-HD Date: Sun, 27 Jan 2019 23:44:58 -1000 Subject: [PATCH 073/185] Test for scheduler determinism --- .../_tests/test_hypothesis_interaction.py | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/pytest_trio/_tests/test_hypothesis_interaction.py b/pytest_trio/_tests/test_hypothesis_interaction.py index 26fe612..c4f0053 100644 --- a/pytest_trio/_tests/test_hypothesis_interaction.py +++ b/pytest_trio/_tests/test_hypothesis_interaction.py @@ -1,6 +1,9 @@ import pytest +import trio from hypothesis import given, settings, strategies as st +from pytest_trio.plugin import _trio_test_runner_factory + # deadline=None avoids unpredictable warnings/errors when CI happens to be # slow (example: https://travis-ci.org/python-trio/pytest-trio/jobs/406738296) # max_examples=5 speeds things up a bit @@ -28,3 +31,47 @@ async def test_mark_outer(n): async def test_mark_and_parametrize(x, y): assert x is None assert y in (1, 2) + + +async def scheduler_trace(): + """Returns a scheduler-dependent value we can use to check determinism.""" + trace = [] + + async def tracer(name): + for i in range(10): + trace.append((name, i)) + await trio.sleep(0) + + async with trio.open_nursery() as nursery: + for i in range(5): + nursery.start_soon(tracer, i) + + return tuple(trace) + + +def test_the_trio_scheduler_is_not_deterministic(): + # At least, not yet. See https://github.com/python-trio/trio/issues/32 + traces = [] + for _ in range(10): + traces.append(trio.run(scheduler_trace)) + assert len(set(traces)) == len(traces) + + +def test_the_trio_scheduler_is_deterministic_under_hypothesis(): + traces = [] + + @our_settings + @given(st.integers()) + @pytest.mark.trio + async def inner(_): + traces.append(await scheduler_trace()) + + # The pytest.mark.trio doesn't do it's magic thing to + # inner functions, so we invoke it explicitly here. + inner.hypothesis.inner_test = _trio_test_runner_factory( + None, inner.hypothesis.inner_test + ) + inner() # Tada, now it's a sync function! + + assert len(traces) >= 5 + assert len(set(traces)) == 1 From 32e1c6cbd8b3792e9e368907581b41a6701af65e Mon Sep 17 00:00:00 2001 From: Zac-HD Date: Mon, 28 Jan 2019 00:20:44 -1000 Subject: [PATCH 074/185] Use Trio's new CancelScope API --- pytest_trio/_tests/test_fixture_ordering.py | 2 +- pytest_trio/plugin.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pytest_trio/_tests/test_fixture_ordering.py b/pytest_trio/_tests/test_fixture_ordering.py index 352ca6a..7693769 100644 --- a/pytest_trio/_tests/test_fixture_ordering.py +++ b/pytest_trio/_tests/test_fixture_ordering.py @@ -171,7 +171,7 @@ async def crash_late_agen(): raise RuntimeError("crash_late_agen".upper()) async def crash(when, token): - with trio.open_cancel_scope(shield=True): + with trio.CancelScope(shield=True): await trio.sleep(when) raise RuntimeError(token.upper()) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 9d86159..0b9b2b4 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -235,7 +235,7 @@ async def run(self, test_ctx, contextvars_ctx): # should cancel them. assert not self.user_done_events func_value = None - with trio.open_cancel_scope() as cancel_scope: + with trio.CancelScope() as cancel_scope: test_ctx.test_cancel_scope = cancel_scope assert not test_ctx.crashed await self._func(**resolved_kwargs) @@ -280,7 +280,7 @@ async def run(self, test_ctx, contextvars_ctx): except BaseException as exc: assert isinstance(exc, trio.Cancelled) test_ctx.crash(None) - with trio.open_cancel_scope(shield=True): + with trio.CancelScope(shield=True): for event in self.user_done_events: await event.wait() From 86b44d2e6d09fd941ca48120a2be223413a41ce0 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Mon, 4 Feb 2019 11:04:12 +1100 Subject: [PATCH 075/185] Final Trio api for deterministic scheduling --- .../_tests/test_hypothesis_interaction.py | 28 +++---------------- pytest_trio/plugin.py | 4 +++ 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/pytest_trio/_tests/test_hypothesis_interaction.py b/pytest_trio/_tests/test_hypothesis_interaction.py index c4f0053..30a37db 100644 --- a/pytest_trio/_tests/test_hypothesis_interaction.py +++ b/pytest_trio/_tests/test_hypothesis_interaction.py @@ -1,5 +1,9 @@ import pytest import trio +from trio.tests.test_scheduler_determinism import ( + scheduler_trace, test_the_trio_scheduler_is_not_deterministic, + test_the_trio_scheduler_is_deterministic_if_seeded +) from hypothesis import given, settings, strategies as st from pytest_trio.plugin import _trio_test_runner_factory @@ -33,30 +37,6 @@ async def test_mark_and_parametrize(x, y): assert y in (1, 2) -async def scheduler_trace(): - """Returns a scheduler-dependent value we can use to check determinism.""" - trace = [] - - async def tracer(name): - for i in range(10): - trace.append((name, i)) - await trio.sleep(0) - - async with trio.open_nursery() as nursery: - for i in range(5): - nursery.start_soon(tracer, i) - - return tuple(trace) - - -def test_the_trio_scheduler_is_not_deterministic(): - # At least, not yet. See https://github.com/python-trio/trio/issues/32 - traces = [] - for _ in range(10): - traces.append(trio.run(scheduler_trace)) - assert len(set(traces)) == len(traces) - - def test_the_trio_scheduler_is_deterministic_under_hypothesis(): traces = [] diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 0b9b2b4..c64839b 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -31,6 +31,10 @@ # even though it uses a module-scoped Random instance. This works # regardless of whether or not the random_module strategy is used. register_random(trio._core._run._r) + # We also have to enable determinism, which is disabled by default + # due to a small performance impact - but fine to enable in testing. + # See https://github.com/python-trio/trio/pull/890/ for details. + trio._core._run._ALLOW_DETERMINISTIC_SCHEDULING = True def pytest_addoption(parser): From 414ec148d20bcde019954738905c8beb8619f520 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Mon, 11 Feb 2019 17:10:31 +1100 Subject: [PATCH 076/185] Require trio >= 0.11 To support determinism under Hypothesis. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2a7e945..d47f5d6 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ packages=find_packages(), entry_points={'pytest11': ['trio = pytest_trio.plugin']}, install_requires=[ - "trio", + "trio >= 0.11", "async_generator >= 1.9", # For node.get_closest_marker "pytest >= 3.6" From 961c59db3db41c2368de09284855241e28ff540f Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Wed, 13 Feb 2019 01:00:25 -0800 Subject: [PATCH 077/185] Bump version to 0.5.2 --- docs/source/history.rst | 12 ++++++++++++ newsfragments/73.feature.rst | 3 --- pytest_trio/_version.py | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) delete mode 100644 newsfragments/73.feature.rst diff --git a/docs/source/history.rst b/docs/source/history.rst index f3e5086..233b6ab 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -5,6 +5,18 @@ Release history .. towncrier release notes start +pytest-trio 0.5.2 (2019-02-13) +------------------------------ + +Features +~~~~~~~~ + +- pytest-trio now makes the Trio scheduler deterministic while running + inside a Hypothesis test. Hopefully you won't see any change, but if + you had scheduler-dependent bugs Hypothesis will be more effective now. (`#73 `__) + +- Updated for compatibility with trio v0.11.0. + pytest-trio 0.5.1 (2018-09-28) ------------------------------ diff --git a/newsfragments/73.feature.rst b/newsfragments/73.feature.rst deleted file mode 100644 index 9993cdf..0000000 --- a/newsfragments/73.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -pytest-trio now makes the Trio scheduler deterministic while running -inside a Hypothesis test. Hopefully you won't see any change, but if -you had scheduler-dependent bugs Hypothesis will be more effective now. diff --git a/pytest_trio/_version.py b/pytest_trio/_version.py index f09277d..236fefc 100644 --- a/pytest_trio/_version.py +++ b/pytest_trio/_version.py @@ -1,3 +1,3 @@ # This file is imported from __init__.py and exec'd from setup.py -__version__ = "0.5.1" +__version__ = "0.5.2" From 550146b099ccecbae9c16218c709866a38d02ee2 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Wed, 13 Feb 2019 01:09:52 -0800 Subject: [PATCH 078/185] bump version to 0.5.2+dev --- pytest_trio/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_trio/_version.py b/pytest_trio/_version.py index 236fefc..d038f19 100644 --- a/pytest_trio/_version.py +++ b/pytest_trio/_version.py @@ -1,3 +1,3 @@ # This file is imported from __init__.py and exec'd from setup.py -__version__ = "0.5.2" +__version__ = "0.5.2+dev" From 0a0296602ccbc1c99e9a4151be99df375ae94d13 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Sun, 9 Jun 2019 23:37:34 -0700 Subject: [PATCH 079/185] In Trio fixtures, 'yield' now acts as a checkpoint. --- docs/source/reference.rst | 108 ++++++++++++++++++++ newsfragments/75.feature.rst | 4 + pytest_trio/_tests/test_fixture_ordering.py | 50 ++++++++- pytest_trio/plugin.py | 7 +- setup.py | 1 + 5 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 newsfragments/75.feature.rst diff --git a/docs/source/reference.rst b/docs/source/reference.rst index 74f0b41..2018b84 100644 --- a/docs/source/reference.rst +++ b/docs/source/reference.rst @@ -106,6 +106,114 @@ but Trio fixtures **must be test scoped**. Class, module, and session scope are not supported. +.. _cancel-yield: + +An important note about ``yield`` fixtures +------------------------------------------ + +Like any pytest fixture, Trio fixtures can contain both setup and +teardown code separated by a ``yield``:: + + @pytest.fixture + async def my_fixture(): + ... setup code ... + yield + ... teardown code ... + +When pytest-trio executes this fixture, it creates a new task, and +runs the setup code until it reaches the ``yield``. Then the fixture's +task goes to sleep. Once the test has finished, the fixture task wakes +up again and resumes at the ``yield``, so it can execute the teardown +code. + +So the ``yield`` in a fixture is sort of like calling ``await +wait_for_test_to_finish()``. And in Trio, any ``await``\-able +operation can be cancelled. For example, we could put a timeout on the +``yield``:: + + @pytest.fixture + async def my_fixture(): + ... setup code ... + with trio.move_on_after(5): + yield # this yield gets cancelled after 5 seconds + ... teardown code ... + +Now if the test takes more than 5 seconds to execute, this fixture +will cancel the ``yield``. + +That's kind of a strange thing to do, but there's another version of +this that's extremely common. Suppose your fixture spawns a background +task, and then the background task raises an exception. Whenever a +background task raises an exception, it automatically cancels +everything inside the nursery's scope – which includes our ``yield``:: + + @pytest.fixture + async def my_fixture(nursery): + nursery.start_soon(function_that_raises_exception) + yield # this yield gets cancelled after the background task crashes + ... teardown code ... + +If you use fixtures with background tasks, you'll probably end up +cancelling one of these ``yield``\s sooner or later. So what happens +if the ``yield`` gets cancelled? + +First, pytest-trio assumes that something has gone wrong and there's +no point in continuing the test. If the top-level test function is +running, then it cancels it. + +Then, pytest-trio waits for the test function to finish, and +then begins tearing down fixtures as normal. + +During this teardown process, it will eventually reach the fixture +that cancelled its ``yield``. This fixture gets resumed to execute its +teardown logic, but with a special twist: since the ``yield`` was +cancelled, the ``yield`` raises :exc:`trio.Cancelled`. + +Now, here's the punchline: this means that in our examples above, the +teardown code might not be executed at all! **This is different from +how pytest fixtures normally work.** Normally, the ``yield`` in a +pytest fixture never raises an exception, so you can be certain that +any code you put after it will execute as normal. But if you have a +fixture with background tasks, and they crash, then your ``yield`` +might raise an exception, and Python will skip executing the code +after the ``yield``. + +In our experience, most fixtures are fine with this, and it prevents +some `weird problems +`__ that can +happen otherwise. But it's something to be aware of. + +If you have a fixture where the ``yield`` might be cancelled but you +still need to run teardown code, then you can use a ``finally`` +block:: + + @pytest.fixture + async def my_fixture(nursery): + nursery.start_soon(function_that_crashes) + try: + # This yield could be cancelled... + yield + finally: + # But this code will run anyway + ... teardown code ... + +(But, watch out: the teardown code is still running in a cancelled +context, so if it has any ``await``\s it could raise +:exc:`trio.Cancelled` again.) + +Or if you use ``with`` to handle teardown, then you don't have to +worry about this because ``with`` blocks always perform cleanup even +if there's an exception:: + + @pytest.fixture + async def my_fixture(nursery): + with get_obj_that_must_be_torn_down() as obj: + nursery.start_soon(function_that_crashes, obj) + # This could raise trio.Cancelled... + # ...but that's OK, the 'with' block will still tear down 'obj' + yield obj + + Concurrent setup/teardown ------------------------- diff --git a/newsfragments/75.feature.rst b/newsfragments/75.feature.rst new file mode 100644 index 0000000..713a6d2 --- /dev/null +++ b/newsfragments/75.feature.rst @@ -0,0 +1,4 @@ +Incompatible change: if you use ``yield`` inside a Trio fixture, and +the ``yield`` gets cancelled (for example, due to a background task +crashing), then the ``yield`` will now raise :exc:`trio.Cancelled`. +See :ref:`cancel-yield` for details. diff --git a/pytest_trio/_tests/test_fixture_ordering.py b/pytest_trio/_tests/test_fixture_ordering.py index 7693769..65b50eb 100644 --- a/pytest_trio/_tests/test_fixture_ordering.py +++ b/pytest_trio/_tests/test_fixture_ordering.py @@ -213,7 +213,8 @@ def test_background_crash_cancellation_propagation(bgmode, testdir): @trio_fixture def crashyfix(nursery): nursery.start_soon(crashy) - yield + with pytest.raises(trio.Cancelled): + yield # We should be cancelled here teardown_deadlines["crashyfix"] = trio.current_effective_deadline() """ @@ -224,7 +225,8 @@ def crashyfix(nursery): async def crashyfix(): async with trio.open_nursery() as nursery: nursery.start_soon(crashy) - await yield_() + with pytest.raises(trio.Cancelled): + await yield_() # We should be cancelled here teardown_deadlines["crashyfix"] = trio.current_effective_deadline() """ @@ -284,3 +286,47 @@ def test_post(): result = testdir.runpytest() result.assert_outcomes(passed=1, failed=1) + + +# See the thread starting at +# https://github.com/python-trio/pytest-trio/pull/77#issuecomment-499979536 +# for details on the real case that this was minimized from +def test_complex_cancel_interaction_regression(testdir): + testdir.makepyfile( + """ + import pytest + import trio + from async_generator import asynccontextmanager, async_generator, yield_ + + async def die_soon(): + raise RuntimeError('oops'.upper()) + + @asynccontextmanager + @async_generator + async def async_finalizer(): + try: + await yield_() + finally: + await trio.sleep(0) + + @pytest.fixture + @async_generator + async def fixture(nursery): + async with trio.open_nursery() as nursery1: + async with async_finalizer(): + async with trio.open_nursery() as nursery2: + nursery2.start_soon(die_soon) + await yield_() + nursery1.cancel_scope.cancel() + + @pytest.mark.trio + async def test_try(fixture): + await trio.sleep_forever() + """ + ) + + result = testdir.runpytest() + result.assert_outcomes(passed=0, failed=1) + result.stdout.fnmatch_lines_random([ + "*OOPS*", + ]) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index c64839b..618ad85 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -4,6 +4,7 @@ from collections.abc import Coroutine, Generator from inspect import iscoroutinefunction, isgeneratorfunction import contextvars +import outcome import pytest import trio from trio.testing import MockClock, trio_test @@ -278,11 +279,13 @@ async def run(self, test_ctx, contextvars_ctx): # code will get it again if it matters), and then use a shield to # keep waiting for the teardown to finish without having to worry # about cancellation. + yield_outcome = outcome.Value(None) try: for event in self.user_done_events: await event.wait() except BaseException as exc: assert isinstance(exc, trio.Cancelled) + yield_outcome = outcome.Error(exc) test_ctx.crash(None) with trio.CancelScope(shield=True): for event in self.user_done_events: @@ -291,14 +294,14 @@ async def run(self, test_ctx, contextvars_ctx): # Do our teardown if isasyncgen(func_value): try: - await func_value.asend(None) + await yield_outcome.asend(func_value) except StopAsyncIteration: pass else: raise RuntimeError("too many yields in fixture") elif isinstance(func_value, Generator): try: - func_value.send(None) + yield_outcome.send(func_value) except StopIteration: pass else: diff --git a/setup.py b/setup.py index d47f5d6..56221b2 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,7 @@ install_requires=[ "trio >= 0.11", "async_generator >= 1.9", + "outcome", # For node.get_closest_marker "pytest >= 3.6" ], From 44a3ab34b16873aeb3b99d1e9e6d29ef2ae09574 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Sun, 9 Jun 2019 23:48:38 -0700 Subject: [PATCH 080/185] If a fixture cancels the test, that's always a failure Even if it doesn't raise an exception afterwards. --- pytest_trio/_tests/test_fixture_mistakes.py | 29 +++++++++++++++++++++ pytest_trio/plugin.py | 26 +++++++++++++++--- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/pytest_trio/_tests/test_fixture_mistakes.py b/pytest_trio/_tests/test_fixture_mistakes.py index 5c6873c..f1c210a 100644 --- a/pytest_trio/_tests/test_fixture_mistakes.py +++ b/pytest_trio/_tests/test_fixture_mistakes.py @@ -117,3 +117,32 @@ def test_whatever(async_fixture): result.stdout.fnmatch_lines( ["*: Trio fixtures can only be used by Trio tests*"] ) + + +@enable_trio_mode +def test_fixture_cancels_test_but_doesnt_raise(testdir, enable_trio_mode): + enable_trio_mode(testdir) + + testdir.makepyfile( + """ + import pytest + import trio + + @pytest.fixture + async def async_fixture(): + with trio.CancelScope() as cscope: + cscope.cancel() + yield + + + async def test_whatever(async_fixture): + pass + """ + ) + + result = testdir.runpytest() + + result.assert_outcomes(failed=1) + result.stdout.fnmatch_lines( + ["*async_fixture*cancelled the test*"] + ) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 618ad85..3cba1d3 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -133,11 +133,16 @@ class TrioTestContext: def __init__(self): self.crashed = False self.test_cancel_scope = None + self.fixtures_with_errors = set() + self.fixtures_with_cancel = set() self.error_list = [] - def crash(self, exc): - if exc is not None: + def crash(self, fixture, exc): + if exc is None: + self.fixtures_with_cancel.add(fixture) + else: self.error_list.append(exc) + self.fixtures_with_errors.add(fixture) self.crashed = True if self.test_cancel_scope is not None: self.test_cancel_scope.cancel() @@ -193,7 +198,7 @@ async def _fixture_manager(self, test_ctx): finally: nursery_fixture.cancel_scope.cancel() except BaseException as exc: - test_ctx.crash(exc) + test_ctx.crash(self, exc) finally: self.setup_done.set() self._teardown_done.set() @@ -286,7 +291,7 @@ async def run(self, test_ctx, contextvars_ctx): except BaseException as exc: assert isinstance(exc, trio.Cancelled) yield_outcome = outcome.Error(exc) - test_ctx.crash(None) + test_ctx.crash(self, None) with trio.CancelScope(shield=True): for event in self.user_done_events: await event.wait() @@ -342,6 +347,19 @@ async def _bootstrap_fixtures_and_run_test(**kwargs): fixture.run, test_ctx, contextvars_ctx, name=fixture.name ) + silent_cancellers = ( + test_ctx.fixtures_with_cancel - test_ctx.fixtures_with_errors + ) + if silent_cancellers: + for fixture in silent_cancellers: + test_ctx.error_list.append( + RuntimeError( + "{} cancelled the test but didn't " + "raise an error" + .format(fixture.name) + ) + ) + if test_ctx.error_list: raise trio.MultiError(test_ctx.error_list) From 00eac210098058ba5fba57cdde9a76d3a25b10bc Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Sun, 9 Jun 2019 23:51:24 -0700 Subject: [PATCH 081/185] Update newsfragment --- newsfragments/75.feature.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/newsfragments/75.feature.rst b/newsfragments/75.feature.rst index 713a6d2..bc13de7 100644 --- a/newsfragments/75.feature.rst +++ b/newsfragments/75.feature.rst @@ -1,4 +1,6 @@ Incompatible change: if you use ``yield`` inside a Trio fixture, and the ``yield`` gets cancelled (for example, due to a background task crashing), then the ``yield`` will now raise :exc:`trio.Cancelled`. -See :ref:`cancel-yield` for details. +See :ref:`cancel-yield` for details. Also, in this same case, +pytest-trio will now reliably mark the test as failed, even if the +fixture doesn't go on to raise an exception. From 95cb0d33262fd2f9a376c71ef9b671e8bc722e94 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Mon, 10 Jun 2019 00:03:37 -0700 Subject: [PATCH 082/185] yapf --- pytest_trio/_tests/test_fixture_mistakes.py | 4 +--- pytest_trio/_tests/test_fixture_ordering.py | 2 +- pytest_trio/plugin.py | 3 +-- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/pytest_trio/_tests/test_fixture_mistakes.py b/pytest_trio/_tests/test_fixture_mistakes.py index f1c210a..d3d0a49 100644 --- a/pytest_trio/_tests/test_fixture_mistakes.py +++ b/pytest_trio/_tests/test_fixture_mistakes.py @@ -143,6 +143,4 @@ async def test_whatever(async_fixture): result = testdir.runpytest() result.assert_outcomes(failed=1) - result.stdout.fnmatch_lines( - ["*async_fixture*cancelled the test*"] - ) + result.stdout.fnmatch_lines(["*async_fixture*cancelled the test*"]) diff --git a/pytest_trio/_tests/test_fixture_ordering.py b/pytest_trio/_tests/test_fixture_ordering.py index 65b50eb..eb73ca9 100644 --- a/pytest_trio/_tests/test_fixture_ordering.py +++ b/pytest_trio/_tests/test_fixture_ordering.py @@ -328,5 +328,5 @@ async def test_try(fixture): result = testdir.runpytest() result.assert_outcomes(passed=0, failed=1) result.stdout.fnmatch_lines_random([ - "*OOPS*", + "*OOPS*", ]) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 3cba1d3..5f96cce 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -355,8 +355,7 @@ async def _bootstrap_fixtures_and_run_test(**kwargs): test_ctx.error_list.append( RuntimeError( "{} cancelled the test but didn't " - "raise an error" - .format(fixture.name) + "raise an error".format(fixture.name) ) ) From 0e30148f704b805ccc2b3429892efebca0d912d0 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Mon, 10 Jun 2019 00:05:56 -0700 Subject: [PATCH 083/185] Fix py35 compat --- pytest_trio/_tests/test_fixture_mistakes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pytest_trio/_tests/test_fixture_mistakes.py b/pytest_trio/_tests/test_fixture_mistakes.py index d3d0a49..6da6abb 100644 --- a/pytest_trio/_tests/test_fixture_mistakes.py +++ b/pytest_trio/_tests/test_fixture_mistakes.py @@ -127,12 +127,14 @@ def test_fixture_cancels_test_but_doesnt_raise(testdir, enable_trio_mode): """ import pytest import trio + from async_generator import async_generator, yield_ @pytest.fixture + @async_generator async def async_fixture(): with trio.CancelScope() as cscope: cscope.cancel() - yield + await yield_() async def test_whatever(async_fixture): From b839d97f444d9b446e633f8bbb71dbb21f92ec1d Mon Sep 17 00:00:00 2001 From: Andrei Pozolotin Date: Sat, 30 Nov 2019 11:46:00 -0600 Subject: [PATCH 084/185] fix assert in magic autojump clock --- docs/source/quickstart.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index e5eac35..f57decb 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -126,8 +126,8 @@ something will happen:: async def test_sleep_efficiently_and_reliably(autojump_clock): start_time = trio.current_time() await trio.sleep(1) - end_time = trio.current_time() - assert start_time - end_time == 1 + finish_time = trio.current_time() + assert finish_time - start_time == 1 In the version of this test we saw before that used real time, at the end we had to use a ``>=`` comparison, in order to account for From 75f1a0dc79a1a40b8e9d22cfb925542cc4d8a446 Mon Sep 17 00:00:00 2001 From: Chris Ostrouchov Date: Fri, 28 Feb 2020 14:11:46 -0500 Subject: [PATCH 085/185] Remove RemovedInPytest4Warning resolved in latest pytest Resolved with issues: https://github.com/pytest-dev/pytest/issues/4039 --- pytest_trio/_tests/conftest.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pytest_trio/_tests/conftest.py b/pytest_trio/_tests/conftest.py index 7e1ccd9..c6481d5 100644 --- a/pytest_trio/_tests/conftest.py +++ b/pytest_trio/_tests/conftest.py @@ -1,11 +1 @@ -# Temporary hack while waiting for an answer here: -# https://github.com/pytest-dev/pytest/issues/4039 -import pytest -import warnings -warnings.filterwarnings( - "default", - category=pytest.RemovedInPytest4Warning, - message=".*non-top-level conftest.*", -) - pytest_plugins = ["pytester"] From f14615e7b986fc602c6a66f18dca6f0bae061b1f Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Wed, 11 Mar 2020 17:16:45 +0400 Subject: [PATCH 086/185] ci: bump tested Python versions --- .travis.yml | 29 ++++++++++++++++++++--------- ci/travis.sh | 39 +-------------------------------------- 2 files changed, 21 insertions(+), 47 deletions(-) diff --git a/.travis.yml b/.travis.yml index c37a900..d4410cc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,23 @@ +os: linux +dist: bionic language: python python: -- 3.5.0 -- 3.5.2 +- pypy3.6-7.2.0 - 3.5-dev - 3.6 - 3.6-dev +- 3.7 - 3.7-dev -sudo: false -dist: trusty -matrix: +- 3.8 +- 3.8-dev +jobs: include: - - os: linux - language: generic - env: USE_PYPY_RELEASE_VERSION=5.9-beta + - python: 3.5.0 + dist: trusty + # The last pytest to support Python 3.5.2 fails to start with recent attrs + # - python: 3.5.2 + # dist: trusty + # Python3.5 on MacOS doesn't currently support TLSv1.2 needed to use pip :( # http://pyfound.blogspot.fr/2017/01/time-to-upgrade-your-python-tls-v12.html # - os: osx @@ -20,7 +25,13 @@ matrix: # env: MACPYTHON=3.5.4 - os: osx language: generic - env: MACPYTHON=3.6.3 + env: MACPYTHON=3.6.8 + - os: osx + language: generic + env: MACPYTHON=3.7.7 + - os: osx + language: generic + env: MACPYTHON=3.8.1 - os: linux language: python python: 3.6 diff --git a/ci/travis.sh b/ci/travis.sh index 6cee6a7..d904d39 100755 --- a/ci/travis.sh +++ b/ci/travis.sh @@ -6,7 +6,7 @@ set -ex YAPF_VERSION=0.22.0 if [ "$TRAVIS_OS_NAME" = "osx" ]; then - curl -Lo macpython.pkg https://www.python.org/ftp/python/${MACPYTHON}/python-${MACPYTHON}-macosx10.6.pkg + curl -Lo macpython.pkg https://www.python.org/ftp/python/${MACPYTHON}/python-${MACPYTHON}-macosx10.9.pkg sudo installer -pkg macpython.pkg -target / ls /Library/Frameworks/Python.framework/Versions/*/bin/ PYTHON_EXE=/Library/Frameworks/Python.framework/Versions/*/bin/python3 @@ -15,43 +15,6 @@ if [ "$TRAVIS_OS_NAME" = "osx" ]; then source testenv/bin/activate fi -if [ "$USE_PYPY_NIGHTLY" = "1" ]; then - curl -fLo pypy.tar.bz2 http://buildbot.pypy.org/nightly/py3.5/pypy-c-jit-latest-linux64.tar.bz2 - if [ ! -s pypy.tar.bz2 ]; then - # We know: - # - curl succeeded (200 response code; -f means "exit with error if - # server returns 4xx or 5xx") - # - nonetheless, pypy.tar.bz2 does not exist, or contains no data - # This isn't going to work, and the failure is not informative of - # anything involving this package. - ls -l - echo "PyPy3 nightly build failed to download – something is wrong on their end." - echo "Skipping testing against the nightly build for right now." - exit 0 - fi - tar xaf pypy.tar.bz2 - # something like "pypy-c-jit-89963-748aa3022295-linux64" - PYPY_DIR=$(echo pypy-c-jit-*) - PYTHON_EXE=$PYPY_DIR/bin/pypy3 - ($PYTHON_EXE -m ensurepip \ - && $PYTHON_EXE -m pip install virtualenv \ - && $PYTHON_EXE -m virtualenv testenv) \ - || (echo "pypy nightly is broken; skipping tests"; exit 0) - source testenv/bin/activate -fi - -if [ "$USE_PYPY_RELEASE_VERSION" != "" ]; then - curl -fLo pypy.tar.bz2 https://bitbucket.org/squeaky/portable-pypy/downloads/pypy3.5-${USE_PYPY_RELEASE_VERSION}-linux_x86_64-portable.tar.bz2 - tar xaf pypy.tar.bz2 - # something like "pypy3.5-5.7.1-beta-linux_x86_64-portable" - PYPY_DIR=$(echo pypy3.5-*) - PYTHON_EXE=$PYPY_DIR/bin/pypy3 - $PYTHON_EXE -m ensurepip - $PYTHON_EXE -m pip install virtualenv - $PYTHON_EXE -m virtualenv testenv - source testenv/bin/activate -fi - pip install -U pip setuptools wheel if [ "$CHECK_FORMATTING" = "1" ]; then From 43668c26430d66b4f8dfc7ac6a5aeb0a03fb860d Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Tue, 19 May 2020 21:03:15 +0400 Subject: [PATCH 087/185] Stop testing Python 3.5 --- .appveyor.yml | 6 ++++-- .travis.yml | 12 ------------ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 1ebc291..29dd725 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -4,10 +4,12 @@ os: Visual Studio 2015 environment: matrix: - - PYTHON: "C:\\Python35" - - PYTHON: "C:\\Python35-x64" - PYTHON: "C:\\Python36" - PYTHON: "C:\\Python36-x64" + - PYTHON: "C:\\Python37" + - PYTHON: "C:\\Python37-x64" + - PYTHON: "C:\\Python38" + - PYTHON: "C:\\Python38-x64" build_script: - "git --no-pager log -n2" diff --git a/.travis.yml b/.travis.yml index d4410cc..c5fd788 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ dist: bionic language: python python: - pypy3.6-7.2.0 -- 3.5-dev - 3.6 - 3.6-dev - 3.7 @@ -12,17 +11,6 @@ python: - 3.8-dev jobs: include: - - python: 3.5.0 - dist: trusty - # The last pytest to support Python 3.5.2 fails to start with recent attrs - # - python: 3.5.2 - # dist: trusty - - # Python3.5 on MacOS doesn't currently support TLSv1.2 needed to use pip :( - # http://pyfound.blogspot.fr/2017/01/time-to-upgrade-your-python-tls-v12.html - # - os: osx - # language: generic - # env: MACPYTHON=3.5.4 - os: osx language: generic env: MACPYTHON=3.6.8 From 9572aaff6010aad79280950392d56155949203d5 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Tue, 19 May 2020 21:06:34 +0400 Subject: [PATCH 088/185] Fix compatibility with Trio 0.15.0 --- pytest_trio/_tests/test_fixture_names.py | 2 +- pytest_trio/plugin.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pytest_trio/_tests/test_fixture_names.py b/pytest_trio/_tests/test_fixture_names.py index f22f64e..54c531c 100644 --- a/pytest_trio/_tests/test_fixture_names.py +++ b/pytest_trio/_tests/test_fixture_names.py @@ -12,7 +12,7 @@ def fixture_with_unique_name(nursery): async def test_fixture_names(fixture_with_unique_name): # This might be a bit fragile ... if we rearrange the nursery hierarchy # somehow so it breaks, then we can make it more robust. - task = trio.hazmat.current_task() + task = trio.lowlevel.current_task() assert task.name == "" sibling_names = {task.name for task in task.parent_nursery.child_tasks} assert "" in sibling_names diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 5f96cce..4901e49 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -208,7 +208,7 @@ async def run(self, test_ctx, contextvars_ctx): # This is a gross hack. I guess Trio should provide a context= # argument to start_soon/start? - task = trio.hazmat.current_task() + task = trio.lowlevel.current_task() assert canary not in task.context task.context = contextvars_ctx # Force a yield so we pick up the new context diff --git a/setup.py b/setup.py index 56221b2..f6798c2 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ packages=find_packages(), entry_points={'pytest11': ['trio = pytest_trio.plugin']}, install_requires=[ - "trio >= 0.11", + "trio >= 0.15.0", "async_generator >= 1.9", "outcome", # For node.get_closest_marker From 4f12e750dc019a6d6156b003a03f5642424b4dfb Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Wed, 20 May 2020 01:15:06 +0000 Subject: [PATCH 089/185] Remove trio-asyncio test --- pytest_trio/_tests/test_trio_asyncio.py | 51 ------------------------- test-requirements.txt | 1 - 2 files changed, 52 deletions(-) delete mode 100644 pytest_trio/_tests/test_trio_asyncio.py diff --git a/pytest_trio/_tests/test_trio_asyncio.py b/pytest_trio/_tests/test_trio_asyncio.py deleted file mode 100644 index 8a3d9d0..0000000 --- a/pytest_trio/_tests/test_trio_asyncio.py +++ /dev/null @@ -1,51 +0,0 @@ -import pytest -import sys -import asyncio -from async_generator import async_generator, yield_ - -try: - import trio_asyncio -except ImportError: - pytestmark = pytest.mark.skip(reason="trio-asyncio not available") - - -async def use_asyncio(): - await trio_asyncio.aio_as_trio(asyncio.sleep)(0) - - -@pytest.fixture() -@async_generator -async def asyncio_loop(): - async with trio_asyncio.open_loop() as loop: - await yield_(loop) - - -@pytest.fixture() -@async_generator -async def asyncio_fixture_with_fixtured_loop(asyncio_loop): - await use_asyncio() - await yield_() - - -@pytest.fixture() -@async_generator -async def asyncio_fixture_own_loop(): - async with trio_asyncio.open_loop(): - await use_asyncio() - await yield_() - - -@pytest.mark.trio -async def test_no_fixture(): - async with trio_asyncio.open_loop(): - await use_asyncio() - - -@pytest.mark.trio -async def test_half_fixtured_asyncpg_conn(asyncio_fixture_own_loop): - await use_asyncio() - - -@pytest.mark.trio -async def test_fixtured_asyncpg_conn(asyncio_fixture_with_fixtured_loop): - await use_asyncio() diff --git a/test-requirements.txt b/test-requirements.txt index af55c2c..b2be0f9 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,3 @@ pytest !=3.7.0, !=3.7.1 # https://github.com/python-trio/pytest-trio/pull/50#issuecomment-413124393 pytest-cov hypothesis>=3.64 -trio-asyncio; python_version >= "3.5.3" From 6dfb7684c0fb961bae6b99e566fee27097c5b1d8 Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Wed, 20 May 2020 01:38:59 +0000 Subject: [PATCH 090/185] Attempt to fix coverage --- ci/travis.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ci/travis.sh b/ci/travis.sh index d904d39..cc4b0cd 100755 --- a/ci/travis.sh +++ b/ci/travis.sh @@ -55,7 +55,9 @@ else mkdir empty cd empty - pytest + # These environment variables ensure that the import of the pytest-trio plugin is covered + # even if pytest-trio is loaded before pytest-cov. See https://pytest-cov.readthedocs.io/en/latest/plugins.html + env COV_CORE_SOURCE=pytest_trio COV_CORE_CONFIG=.coveragerc COV_CORE_DATAFILE=.coverage pytest bash <(curl -s https://codecov.io/bash) fi From 8a719e611fa1f6d3facfb718192bc51fe55a2482 Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Wed, 20 May 2020 01:56:34 +0000 Subject: [PATCH 091/185] Bump version and run towncrier for 0.6.0 release --- docs/source/history.rst | 16 ++++++++++++++++ newsfragments/75.feature.rst | 6 ------ pytest_trio/_version.py | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) delete mode 100644 newsfragments/75.feature.rst diff --git a/docs/source/history.rst b/docs/source/history.rst index 233b6ab..c707e82 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -5,6 +5,22 @@ Release history .. towncrier release notes start +pytest-trio 0.6.0 (2020-05-20) +---------------------------------- + +Features +~~~~~~~~ + +- Incompatible change: if you use ``yield`` inside a Trio fixture, and + the ``yield`` gets cancelled (for example, due to a background task + crashing), then the ``yield`` will now raise :exc:`trio.Cancelled`. + See :ref:`cancel-yield` for details. Also, in this same case, + pytest-trio will now reliably mark the test as failed, even if the + fixture doesn't go on to raise an exception. (`#75 `__) + +- Updated for compatibility with Trio v0.15.0. + + pytest-trio 0.5.2 (2019-02-13) ------------------------------ diff --git a/newsfragments/75.feature.rst b/newsfragments/75.feature.rst deleted file mode 100644 index bc13de7..0000000 --- a/newsfragments/75.feature.rst +++ /dev/null @@ -1,6 +0,0 @@ -Incompatible change: if you use ``yield`` inside a Trio fixture, and -the ``yield`` gets cancelled (for example, due to a background task -crashing), then the ``yield`` will now raise :exc:`trio.Cancelled`. -See :ref:`cancel-yield` for details. Also, in this same case, -pytest-trio will now reliably mark the test as failed, even if the -fixture doesn't go on to raise an exception. diff --git a/pytest_trio/_version.py b/pytest_trio/_version.py index d038f19..ae10048 100644 --- a/pytest_trio/_version.py +++ b/pytest_trio/_version.py @@ -1,3 +1,3 @@ # This file is imported from __init__.py and exec'd from setup.py -__version__ = "0.5.2+dev" +__version__ = "0.6.0" From eb4673f356d008d670d0f4842b572ed2b08b474a Mon Sep 17 00:00:00 2001 From: Joshua Oreman Date: Wed, 20 May 2020 02:15:04 +0000 Subject: [PATCH 092/185] Bump version to 0.6.0+dev post release --- pytest_trio/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_trio/_version.py b/pytest_trio/_version.py index ae10048..dff3253 100644 --- a/pytest_trio/_version.py +++ b/pytest_trio/_version.py @@ -1,3 +1,3 @@ # This file is imported from __init__.py and exec'd from setup.py -__version__ = "0.6.0" +__version__ = "0.6.0+dev" From 45c4aa9fa8c78d49b63a25e9e74a183919dae5f0 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Wed, 20 May 2020 14:09:44 +0400 Subject: [PATCH 093/185] Remove Python 3.5 support --- pytest_trio/_tests/test_async_yield_fixture.py | 16 ++++++---------- pytest_trio/plugin.py | 6 ------ 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/pytest_trio/_tests/test_async_yield_fixture.py b/pytest_trio/_tests/test_async_yield_fixture.py index 4810456..95d2df7 100644 --- a/pytest_trio/_tests/test_async_yield_fixture.py +++ b/pytest_trio/_tests/test_async_yield_fixture.py @@ -6,17 +6,13 @@ @pytest.fixture(params=['Python>=36', 'async_generator']) def async_yield_implementation(request): if request.param == 'Python>=36': - if sys.version_info < (3, 6): - pytest.skip("requires python3.6") - else: + def patch_code(code): + # Convert code to use Python>=3.6 builtin async generator + code = re.sub(r'(?m)^\s*@async_generator\n', r'', code) + code = re.sub(r'await yield_', r'yield', code) + return code - def patch_code(code): - # Convert code to use Python>=3.6 builtin async generator - code = re.sub(r'(?m)^\s*@async_generator\n', r'', code) - code = re.sub(r'await yield_', r'yield', code) - return code - - return patch_code + return patch_code else: return lambda x: x diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 4901e49..12e774b 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -17,12 +17,6 @@ # Basic setup ################################################################ -if sys.version_info >= (3, 6): - ORDERED_DICTS = True -else: - # Ordered dict (and **kwargs) not available with Python<3.6 - ORDERED_DICTS = False - try: from hypothesis import register_random except ImportError: # pragma: no cover From 651e55884aed362fd876b9aa134c02f66d390798 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Wed, 20 May 2020 14:17:53 +0400 Subject: [PATCH 094/185] yapf --- pytest_trio/_tests/test_async_yield_fixture.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pytest_trio/_tests/test_async_yield_fixture.py b/pytest_trio/_tests/test_async_yield_fixture.py index 95d2df7..4fa6098 100644 --- a/pytest_trio/_tests/test_async_yield_fixture.py +++ b/pytest_trio/_tests/test_async_yield_fixture.py @@ -6,6 +6,7 @@ @pytest.fixture(params=['Python>=36', 'async_generator']) def async_yield_implementation(request): if request.param == 'Python>=36': + def patch_code(code): # Convert code to use Python>=3.6 builtin async generator code = re.sub(r'(?m)^\s*@async_generator\n', r'', code) From b519b0f5fd4eddbc6e5bb543db8c904557cc8ac3 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 22 Aug 2020 21:10:04 +0000 Subject: [PATCH 095/185] Update python_requires to >= 3.6 Trio >= 0.15.0 is required and that in turn requires Python >= 3.6 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f6798c2..054ffca 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ 'testing', 'trio', ], - python_requires=">=3.5", + python_requires=">=3.6", classifiers=[ "License :: OSI Approved :: MIT License", "License :: OSI Approved :: Apache Software License", From bfef85c948cfe6bf19c78baa2d497caf7b56d81a Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 22 Aug 2020 17:58:08 -0400 Subject: [PATCH 096/185] Update .assert_outcomes() calls for pytest 6.0 https://github.com/python-trio/pytest-trio/pull/98#issuecomment-678699693 --- pytest_trio/_tests/test_fixture_mistakes.py | 8 ++++---- test-requirements.txt | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pytest_trio/_tests/test_fixture_mistakes.py b/pytest_trio/_tests/test_fixture_mistakes.py index 6da6abb..5521a80 100644 --- a/pytest_trio/_tests/test_fixture_mistakes.py +++ b/pytest_trio/_tests/test_fixture_mistakes.py @@ -34,7 +34,7 @@ def test_sync_indirect(indirect_trio_time): result = testdir.runpytest() - result.assert_outcomes(passed=1, error=2) + result.assert_outcomes(passed=1, errors=2) result.stdout.fnmatch_lines( ["*: Trio fixtures can only be used by Trio tests*"] ) @@ -65,7 +65,7 @@ async def test_foo(self, async_class_fixture): result = testdir.runpytest() - result.assert_outcomes(error=1) + result.assert_outcomes(errors=1) result.stdout.fnmatch_lines(["*: Trio fixtures must be function-scope*"]) @@ -89,7 +89,7 @@ async def test_whatever(async_session_fixture): result = testdir.runpytest() - result.assert_outcomes(error=1) + result.assert_outcomes(errors=1) result.stdout.fnmatch_lines(["*: Trio fixtures must be function-scope*"]) @@ -113,7 +113,7 @@ def test_whatever(async_fixture): result = testdir.runpytest() - result.assert_outcomes(error=1) + result.assert_outcomes(errors=1) result.stdout.fnmatch_lines( ["*: Trio fixtures can only be used by Trio tests*"] ) diff --git a/test-requirements.txt b/test-requirements.txt index b2be0f9..5c53c25 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,3 @@ -pytest !=3.7.0, !=3.7.1 # https://github.com/python-trio/pytest-trio/pull/50#issuecomment-413124393 +pytest >= 6.0.0 # https://github.com/python-trio/pytest-trio/pull/98#issuecomment-678699693 pytest-cov hypothesis>=3.64 From 1993ad7ca10ee602c8154d211cf2c65433d03fff Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 22 Aug 2020 20:25:12 -0400 Subject: [PATCH 097/185] Update quickstart for raw pytest async def behavior --- docs/source/quickstart.rst | 68 +++++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index e5eac35..16f16a1 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -26,43 +26,36 @@ fail:: async def test_should_fail(): assert False -If we run this under pytest normally, then we get a strange result: +If we run this under pytest normally, then the tests are skipped and we get +warnings. Note that in versions of pytest prior to v4.4.0 the tests end up +being reported as passing with other warnings despite not actually having +been properly run. .. code-block:: none $ pytest test_example.py - ======================== test session starts ========================= - platform linux -- Python 3.6.5, pytest-3.6.3, py-1.5.4, pluggy-0.6.0 - rootdir: /tmp, inifile: + platform linux -- Python 3.8.5, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 + rootdir: /tmp collected 2 items - test_example.py .. [100%] + test_example.py ss [100%] ========================== warnings summary ========================== test_example.py::test_sleep - .../_pytest/python.py:196: RuntimeWarning: coroutine 'test_sleep' was never awaited - testfunction(**testargs) - test_example.py::test_should_fail - .../_pytest/python.py:196: RuntimeWarning: coroutine 'test_should_fail' was never awaited - testfunction(**testargs) - - -- Docs: http://doc.pytest.org/en/latest/warnings.html - ================ 2 passed, 2 warnings in 0.02 seconds ================ - -So ``test_sleep`` passed, which is what we expected... but -``test_should_fail`` also passes, which is strange. And it says that -the whole test run completed in 0.02 seconds, which is weird, because -``test_sleep`` should have taken at least second to run. And then -there are these strange warnings at the bottom... what's going on -here? - -The problem is that our tests are async, and pytest doesn't know what -to do with it. So it basically skips running them entirely, and then -reports them as passed. This is not very helpful! If you see warnings -like this, or if your tests seem to pass but your coverage reports -claim that they weren't run at all, then this might be the problem. + .../_pytest/python.py:169: PytestUnhandledCoroutineWarning: async + def functions are not natively supported and have been skipped. + You need to install a suitable plugin for your async framework, for + example: + - pytest-asyncio + - pytest-trio + - pytest-tornasync + - pytest-twisted + warnings.warn(PytestUnhandledCoroutineWarning(msg.format(nodeid))) + + -- Docs: https://docs.pytest.org/en/stable/warnings.html + =================== 2 skipped, 2 warnings in 0.26s =================== Here's the fix: @@ -89,9 +82,9 @@ And we're done! Let's try running pytest again: $ pytest test_example.py ======================== test session starts ========================= - platform linux -- Python 3.6.5, pytest-3.6.3, py-1.5.4, pluggy-0.6.0 - rootdir: /tmp, inifile: pytest.ini - plugins: trio-0.4.2 + platform linux -- Python 3.8.5, pytest-6.0.1, py-1.9.0, pluggy-0.13.1 + rootdir: /tmp, configfile: pytest.ini + plugins: trio-0.6.0 collected 2 items test_example.py .F [100%] @@ -99,12 +92,25 @@ And we're done! Let's try running pytest again: ============================== FAILURES ============================== __________________________ test_should_fail __________________________ + value = + + async def yield_(value=None): + > return await _yield_(value) + + venv/lib/python3.8/site-packages/async_generator/_impl.py:106: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + venv/lib/python3.8/site-packages/async_generator/_impl.py:99: in _yield_ + return (yield _wrap(value)) + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + async def test_should_fail(): > assert False E assert False - test_example.py:7: AssertionError - ================= 1 failed, 1 passed in 1.05 seconds ================= + test_example.py:11: AssertionError + ====================== short test summary info ======================= + FAILED test_example.py::test_should_fail - assert False + ==================== 1 failed, 1 passed in 1.23s ===================== Notice that now it says ``plugins: trio``, which means that pytest-trio is installed, and the results make sense: the good test From a16195eb207d35d9d8db59a1d123307c01df3862 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 22 Aug 2020 20:29:16 -0400 Subject: [PATCH 098/185] expound --- docs/source/quickstart.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 16f16a1..872e869 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -27,7 +27,8 @@ fail:: assert False If we run this under pytest normally, then the tests are skipped and we get -warnings. Note that in versions of pytest prior to v4.4.0 the tests end up +warnings explaining how pytest itself does not directly support async def +tests. Note that in versions of pytest prior to v4.4.0 the tests end up being reported as passing with other warnings despite not actually having been properly run. From 38da6a827a6b272b9329d76ef445205f268881c3 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 22 Aug 2020 20:42:33 -0400 Subject: [PATCH 099/185] a warning --- docs/source/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 872e869..cb4a92d 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -27,7 +27,7 @@ fail:: assert False If we run this under pytest normally, then the tests are skipped and we get -warnings explaining how pytest itself does not directly support async def +a warning explaining how pytest itself does not directly support async def tests. Note that in versions of pytest prior to v4.4.0 the tests end up being reported as passing with other warnings despite not actually having been properly run. From 4d1176fbc8b7a3835912fdefbbbe9e3bd39b9899 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 22 Aug 2020 21:30:35 -0400 Subject: [PATCH 100/185] Remove extraneous "figure out" Or was it the "know"... --- docs/source/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index e5eac35..d0e45d8 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -332,7 +332,7 @@ this is easy: if you request port 0, then the operating system will pick an unused one for you automatically. Problem solved! But wait... if the operating system is picking the port for us, how do -we know figure out which one it picked, so we can connect to it later? +we know which one it picked, so we can connect to it later? Well, there's no way to predict the port ahead of time. But after :func:`~trio.serve_tcp` has opened a port, it can check and see what From a8ee6daa4afa6cd28ea37b7fdd001f4462844a40 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 25 Aug 2020 19:39:03 -0400 Subject: [PATCH 101/185] Fix a few missed .assert_outcomes(errors=) Given they are failed not failures, shouldn't these have been erred or errored not errors? Oh well. --- pytest_trio/_tests/test_async_fixture.py | 2 +- pytest_trio/_tests/test_async_yield_fixture.py | 2 +- pytest_trio/_tests/test_basic.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pytest_trio/_tests/test_async_fixture.py b/pytest_trio/_tests/test_async_fixture.py index 8ca95c4..8fb5875 100644 --- a/pytest_trio/_tests/test_async_fixture.py +++ b/pytest_trio/_tests/test_async_fixture.py @@ -139,4 +139,4 @@ async def test_base(fix1): result = testdir.runpytest() - result.assert_outcomes(error=1) + result.assert_outcomes(errors=1) diff --git a/pytest_trio/_tests/test_async_yield_fixture.py b/pytest_trio/_tests/test_async_yield_fixture.py index 4fa6098..71dd362 100644 --- a/pytest_trio/_tests/test_async_yield_fixture.py +++ b/pytest_trio/_tests/test_async_yield_fixture.py @@ -259,7 +259,7 @@ async def test_actual_test(fix1): result = testdir.runpytest() # TODO: should trigger error instead of failure - # result.assert_outcomes(error=1) + # result.assert_outcomes(errors=1) result.assert_outcomes(failed=1) diff --git a/pytest_trio/_tests/test_basic.py b/pytest_trio/_tests/test_basic.py index d442ee6..c687d53 100644 --- a/pytest_trio/_tests/test_basic.py +++ b/pytest_trio/_tests/test_basic.py @@ -73,4 +73,4 @@ def test_invalid(): result = testdir.runpytest() - result.assert_outcomes(error=1) + result.assert_outcomes(errors=1) From 4279e12063bb4f2114ba25db4bdcb13c6022808d Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 26 Aug 2020 22:11:38 -0400 Subject: [PATCH 102/185] Add trio_run for configurable substitution for trio.run --- pytest_trio/_tests/helpers.py | 6 ++++- pytest_trio/plugin.py | 42 ++++++++++++++++++++++++++++++----- test-requirements.txt | 1 + 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/pytest_trio/_tests/helpers.py b/pytest_trio/_tests/helpers.py index 5ae5f9e..89ef16c 100644 --- a/pytest_trio/_tests/helpers.py +++ b/pytest_trio/_tests/helpers.py @@ -5,11 +5,15 @@ def enable_trio_mode_via_pytest_ini(testdir): testdir.makefile(".ini", pytest="[pytest]\ntrio_mode = true\n") +def enable_qtrio_mode_via_pytest_ini(testdir): + testdir.makefile(".ini", pytest="[pytest]\ntrio_mode = true\ntrio_run = qtrio\n") + + def enable_trio_mode_via_conftest_py(testdir): testdir.makeconftest("from pytest_trio.enable_trio_mode import *") enable_trio_mode = pytest.mark.parametrize( "enable_trio_mode", - [enable_trio_mode_via_pytest_ini, enable_trio_mode_via_conftest_py] + [enable_trio_mode_via_pytest_ini, enable_qtrio_mode_via_pytest_ini, enable_trio_mode_via_conftest_py] ) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 12e774b..4ec673f 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -39,6 +39,12 @@ def pytest_addoption(parser): type="bool", default=False, ) + parser.addini( + "trio_run", + "what runner should pytest-trio use? [trio, qtrio]", + type="linelist", + default=["trio"], + ) def pytest_configure(config): @@ -308,7 +314,23 @@ async def run(self, test_ctx, contextvars_ctx): def _trio_test_runner_factory(item, testfunc=None): - testfunc = testfunc or item.obj + if testfunc: + run = trio.run + else: + testfunc = item.obj + + runs = {marker.kwargs.get('run', trio.run) for marker in item.iter_markers("trio")} + + if len(runs) == 0: + 1/0 + elif len(runs) == 1: + [run] = runs + else: + runs.discard(trio.run) + if len(runs) == 1: + [run] = runs + else: + 1/0 if getattr(testfunc, '_trio_test_runner_wrapped', False): # We have already wrapped this, perhaps because we combined Hypothesis @@ -320,7 +342,7 @@ def _trio_test_runner_factory(item, testfunc=None): 'test function `%r` is marked trio but is not async' % item ) - @trio_test + @trio_test(run=run) async def _bootstrap_fixtures_and_run_test(**kwargs): __tracebackhide__ = True @@ -438,19 +460,29 @@ def pytest_fixture_setup(fixturedef, request): ################################################################ -def automark(items): +def automark(items, run=trio.run): for item in items: if hasattr(item.obj, "hypothesis"): test_func = item.obj.hypothesis.inner_test else: test_func = item.obj if iscoroutinefunction(test_func): - item.add_marker(pytest.mark.trio) + item.add_marker(pytest.mark.trio(run=run)) def pytest_collection_modifyitems(config, items): if config.getini("trio_mode"): - automark(items) + [run_string] = config.getini("trio_run") + + if run_string == "trio": + run = trio.run + elif run_string == "qtrio": + import qtrio + run = qtrio.run + else: + 1/0 + + automark(items, run=run) ################################################################ diff --git a/test-requirements.txt b/test-requirements.txt index 5c53c25..9139fcf 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,4 @@ pytest >= 6.0.0 # https://github.com/python-trio/pytest-trio/pull/98#issuecomment-678699693 pytest-cov hypothesis>=3.64 +https://github.com/altendky/trio/archive/configurable_trio_run.zip From 3d213fe004be66247977b64e572f353ad0a89050 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 26 Aug 2020 23:10:57 -0400 Subject: [PATCH 103/185] --upgrade for the Trio PR --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 9139fcf..19ee938 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,4 @@ pytest >= 6.0.0 # https://github.com/python-trio/pytest-trio/pull/98#issuecomment-678699693 pytest-cov hypothesis>=3.64 -https://github.com/altendky/trio/archive/configurable_trio_run.zip +--upgrade https://github.com/altendky/trio/archive/configurable_trio_run.zip From 5b1448dbd22c78f6b08f7993f02c473b6327a622 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 26 Aug 2020 23:18:28 -0400 Subject: [PATCH 104/185] Revert "--upgrade for the Trio PR" This reverts commit 3d213fe004be66247977b64e572f353ad0a89050. --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 19ee938..9139fcf 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,4 @@ pytest >= 6.0.0 # https://github.com/python-trio/pytest-trio/pull/98#issuecomment-678699693 pytest-cov hypothesis>=3.64 ---upgrade https://github.com/altendky/trio/archive/configurable_trio_run.zip +https://github.com/altendky/trio/archive/configurable_trio_run.zip From 05aa01dd887d2dedf7d9b2c97ac44834b9e26c8c Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 26 Aug 2020 23:21:21 -0400 Subject: [PATCH 105/185] add qtrio for testing --- test-requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/test-requirements.txt b/test-requirements.txt index 9139fcf..daa6edd 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,3 +2,4 @@ pytest >= 6.0.0 # https://github.com/python-trio/pytest-trio/pull/98#issuecomme pytest-cov hypothesis>=3.64 https://github.com/altendky/trio/archive/configurable_trio_run.zip +https://github.com/altendky/qtrio/archive/9-altendky-pytest_trio.zip From 135476168665e985932c9af53d7e05bcbbfbeecb Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 26 Aug 2020 23:44:23 -0400 Subject: [PATCH 106/185] Update test-requirements.txt --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index daa6edd..6b83b37 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,4 +2,4 @@ pytest >= 6.0.0 # https://github.com/python-trio/pytest-trio/pull/98#issuecomme pytest-cov hypothesis>=3.64 https://github.com/altendky/trio/archive/configurable_trio_run.zip -https://github.com/altendky/qtrio/archive/9-altendky-pytest_trio.zip +https://github.com/altendky/qtrio/archive/9-altendky-pytest_trio.zip[pyside2] From d128020295abf3689e54f5cdf18462b860db637f Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 27 Aug 2020 00:17:49 -0400 Subject: [PATCH 107/185] Update test-requirements.txt --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 6b83b37..329fe63 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,4 +2,4 @@ pytest >= 6.0.0 # https://github.com/python-trio/pytest-trio/pull/98#issuecomme pytest-cov hypothesis>=3.64 https://github.com/altendky/trio/archive/configurable_trio_run.zip -https://github.com/altendky/qtrio/archive/9-altendky-pytest_trio.zip[pyside2] +"qtrio[pyside2] @ https://github.com/altendky/qtrio/archive/9-altendky-pytest_trio.zip" From b6b7d0107c27e8e9806fd7cb1cb742d33b6fbec0 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 27 Aug 2020 08:47:09 -0400 Subject: [PATCH 108/185] no "s i guess --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 329fe63..d391c30 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,4 +2,4 @@ pytest >= 6.0.0 # https://github.com/python-trio/pytest-trio/pull/98#issuecomme pytest-cov hypothesis>=3.64 https://github.com/altendky/trio/archive/configurable_trio_run.zip -"qtrio[pyside2] @ https://github.com/altendky/qtrio/archive/9-altendky-pytest_trio.zip" +qtrio[pyside2] @ https://github.com/altendky/qtrio/archive/9-altendky-pytest_trio.zip From a021e3f650e10c6db046e78bd08f384fe6b63263 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 27 Aug 2020 11:20:53 -0400 Subject: [PATCH 109/185] use fake qtrio for test to avoid the complicated system setup --- pytest_trio/_tests/helpers.py | 6 +++--- pytest_trio/_tests/test_trio_mode.py | 31 ++++++++++++++++++++++++++++ test-requirements.txt | 1 - 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/pytest_trio/_tests/helpers.py b/pytest_trio/_tests/helpers.py index 89ef16c..dfba5d1 100644 --- a/pytest_trio/_tests/helpers.py +++ b/pytest_trio/_tests/helpers.py @@ -5,8 +5,8 @@ def enable_trio_mode_via_pytest_ini(testdir): testdir.makefile(".ini", pytest="[pytest]\ntrio_mode = true\n") -def enable_qtrio_mode_via_pytest_ini(testdir): - testdir.makefile(".ini", pytest="[pytest]\ntrio_mode = true\ntrio_run = qtrio\n") +def enable_trio_mode_trio_run_via_pytest_ini(testdir): + testdir.makefile(".ini", pytest="[pytest]\ntrio_mode = true\ntrio_run = trio\n") def enable_trio_mode_via_conftest_py(testdir): @@ -15,5 +15,5 @@ def enable_trio_mode_via_conftest_py(testdir): enable_trio_mode = pytest.mark.parametrize( "enable_trio_mode", - [enable_trio_mode_via_pytest_ini, enable_qtrio_mode_via_pytest_ini, enable_trio_mode_via_conftest_py] + [enable_trio_mode_via_pytest_ini, enable_trio_mode_trio_run_via_pytest_ini, enable_trio_mode_via_conftest_py] ) diff --git a/pytest_trio/_tests/test_trio_mode.py b/pytest_trio/_tests/test_trio_mode.py index efc66a8..cef4bda 100644 --- a/pytest_trio/_tests/test_trio_mode.py +++ b/pytest_trio/_tests/test_trio_mode.py @@ -36,3 +36,34 @@ def test_trio_mode(testdir, enable_trio_mode): result = testdir.runpytest() result.assert_outcomes(passed=2, failed=2) + + +def test_qtrio_mode_configuration(testdir): + testdir.makefile(".ini", pytest="[pytest]\ntrio_mode = true\ntrio_run = qtrio\n") + + qtrio_text = """ + import trio + + fake_used = False + + def run(*args, **kwargs): + global fake_used + fake_used = True + + return trio.run(*args, **kwargs) + """ + + testdir.makepyfile(qtrio=qtrio_text) + + test_text = """ + import qtrio + import trio + + async def test_fake_qtrio_used(): + await trio.sleep(0) + assert qtrio.fake_used + """ + testdir.makepyfile(test_text) + + result = testdir.runpytest() + result.assert_outcomes(passed=1) diff --git a/test-requirements.txt b/test-requirements.txt index d391c30..9139fcf 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,4 +2,3 @@ pytest >= 6.0.0 # https://github.com/python-trio/pytest-trio/pull/98#issuecomme pytest-cov hypothesis>=3.64 https://github.com/altendky/trio/archive/configurable_trio_run.zip -qtrio[pyside2] @ https://github.com/altendky/qtrio/archive/9-altendky-pytest_trio.zip From 9d0572845d2c9410bb2abb7987273b2c5bbe2221 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 27 Aug 2020 11:38:32 -0400 Subject: [PATCH 110/185] yapf --- pytest_trio/_tests/helpers.py | 10 ++++++++-- pytest_trio/_tests/test_trio_mode.py | 4 +++- pytest_trio/plugin.py | 11 +++++++---- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/pytest_trio/_tests/helpers.py b/pytest_trio/_tests/helpers.py index dfba5d1..ac85f73 100644 --- a/pytest_trio/_tests/helpers.py +++ b/pytest_trio/_tests/helpers.py @@ -6,7 +6,9 @@ def enable_trio_mode_via_pytest_ini(testdir): def enable_trio_mode_trio_run_via_pytest_ini(testdir): - testdir.makefile(".ini", pytest="[pytest]\ntrio_mode = true\ntrio_run = trio\n") + testdir.makefile( + ".ini", pytest="[pytest]\ntrio_mode = true\ntrio_run = trio\n" + ) def enable_trio_mode_via_conftest_py(testdir): @@ -15,5 +17,9 @@ def enable_trio_mode_via_conftest_py(testdir): enable_trio_mode = pytest.mark.parametrize( "enable_trio_mode", - [enable_trio_mode_via_pytest_ini, enable_trio_mode_trio_run_via_pytest_ini, enable_trio_mode_via_conftest_py] + [ + enable_trio_mode_via_pytest_ini, + enable_trio_mode_trio_run_via_pytest_ini, + enable_trio_mode_via_conftest_py, + ] ) diff --git a/pytest_trio/_tests/test_trio_mode.py b/pytest_trio/_tests/test_trio_mode.py index cef4bda..26bba62 100644 --- a/pytest_trio/_tests/test_trio_mode.py +++ b/pytest_trio/_tests/test_trio_mode.py @@ -39,7 +39,9 @@ def test_trio_mode(testdir, enable_trio_mode): def test_qtrio_mode_configuration(testdir): - testdir.makefile(".ini", pytest="[pytest]\ntrio_mode = true\ntrio_run = qtrio\n") + testdir.makefile( + ".ini", pytest="[pytest]\ntrio_mode = true\ntrio_run = qtrio\n" + ) qtrio_text = """ import trio diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 4ec673f..124ab67 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -319,10 +319,13 @@ def _trio_test_runner_factory(item, testfunc=None): else: testfunc = item.obj - runs = {marker.kwargs.get('run', trio.run) for marker in item.iter_markers("trio")} + runs = { + marker.kwargs.get('run', trio.run) + for marker in item.iter_markers("trio") + } if len(runs) == 0: - 1/0 + 1 / 0 elif len(runs) == 1: [run] = runs else: @@ -330,7 +333,7 @@ def _trio_test_runner_factory(item, testfunc=None): if len(runs) == 1: [run] = runs else: - 1/0 + 1 / 0 if getattr(testfunc, '_trio_test_runner_wrapped', False): # We have already wrapped this, perhaps because we combined Hypothesis @@ -480,7 +483,7 @@ def pytest_collection_modifyitems(config, items): import qtrio run = qtrio.run else: - 1/0 + 1 / 0 automark(items, run=run) From 1104289dcc223652a68e01944b2c65e79ba78f0c Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 27 Aug 2020 13:17:23 -0400 Subject: [PATCH 111/185] yapf (actually) --- pytest_trio/_tests/helpers.py | 3 +-- pytest_trio/_tests/test_async_yield_fixture.py | 8 ++++---- pytest_trio/plugin.py | 1 - 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pytest_trio/_tests/helpers.py b/pytest_trio/_tests/helpers.py index ac85f73..3715fa5 100644 --- a/pytest_trio/_tests/helpers.py +++ b/pytest_trio/_tests/helpers.py @@ -16,8 +16,7 @@ def enable_trio_mode_via_conftest_py(testdir): enable_trio_mode = pytest.mark.parametrize( - "enable_trio_mode", - [ + "enable_trio_mode", [ enable_trio_mode_via_pytest_ini, enable_trio_mode_trio_run_via_pytest_ini, enable_trio_mode_via_conftest_py, diff --git a/pytest_trio/_tests/test_async_yield_fixture.py b/pytest_trio/_tests/test_async_yield_fixture.py index 71dd362..126cfb9 100644 --- a/pytest_trio/_tests/test_async_yield_fixture.py +++ b/pytest_trio/_tests/test_async_yield_fixture.py @@ -122,7 +122,7 @@ def test_after(): def test_async_yield_fixture_within_sync_fixture( - testdir, async_yield_implementation + testdir, async_yield_implementation ): testdir.makepyfile( @@ -174,7 +174,7 @@ def test_after(): def test_async_yield_fixture_within_sync_yield_fixture( - testdir, async_yield_implementation + testdir, async_yield_implementation ): testdir.makepyfile( @@ -231,7 +231,7 @@ def test_after(): def test_async_yield_fixture_with_multiple_yields( - testdir, async_yield_implementation + testdir, async_yield_implementation ): testdir.makepyfile( @@ -304,7 +304,7 @@ async def test_actual_test(server): def test_async_yield_fixture_crashed_teardown_allow_other_teardowns( - testdir, async_yield_implementation + testdir, async_yield_implementation ): testdir.makepyfile( diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 124ab67..e38ee28 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -156,7 +156,6 @@ class TrioFixture: test itself as well, since the test is basically just a fixture with no dependents and no teardown. """ - def __init__(self, name, func, pytest_kwargs, is_test=False): self.name = name self._func = func From 8cd81b06b5687f7263203eb787462b13d609951c Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 27 Aug 2020 13:24:04 -0400 Subject: [PATCH 112/185] yapf (again (again)) (but with 0.22.0) --- pytest_trio/_tests/test_async_yield_fixture.py | 8 ++++---- pytest_trio/plugin.py | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pytest_trio/_tests/test_async_yield_fixture.py b/pytest_trio/_tests/test_async_yield_fixture.py index 126cfb9..71dd362 100644 --- a/pytest_trio/_tests/test_async_yield_fixture.py +++ b/pytest_trio/_tests/test_async_yield_fixture.py @@ -122,7 +122,7 @@ def test_after(): def test_async_yield_fixture_within_sync_fixture( - testdir, async_yield_implementation + testdir, async_yield_implementation ): testdir.makepyfile( @@ -174,7 +174,7 @@ def test_after(): def test_async_yield_fixture_within_sync_yield_fixture( - testdir, async_yield_implementation + testdir, async_yield_implementation ): testdir.makepyfile( @@ -231,7 +231,7 @@ def test_after(): def test_async_yield_fixture_with_multiple_yields( - testdir, async_yield_implementation + testdir, async_yield_implementation ): testdir.makepyfile( @@ -304,7 +304,7 @@ async def test_actual_test(server): def test_async_yield_fixture_crashed_teardown_allow_other_teardowns( - testdir, async_yield_implementation + testdir, async_yield_implementation ): testdir.makepyfile( diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index e38ee28..124ab67 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -156,6 +156,7 @@ class TrioFixture: test itself as well, since the test is basically just a fixture with no dependents and no teardown. """ + def __init__(self, name, func, pytest_kwargs, is_test=False): self.name = name self._func = func From 1ebf01809639e6415ca305439647b515121062e2 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 27 Aug 2020 14:13:12 -0400 Subject: [PATCH 113/185] Move trio_test() here --- pytest_trio/plugin.py | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 124ab67..a86840e 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -1,4 +1,5 @@ """pytest-trio implementation.""" +from functools import wraps, partial import sys from traceback import format_exception from collections.abc import Coroutine, Generator @@ -7,7 +8,8 @@ import outcome import pytest import trio -from trio.testing import MockClock, trio_test +from trio.abc import Clock, Instrument +from trio.testing import MockClock from async_generator import ( async_generator, yield_, asynccontextmanager, isasyncgen, isasyncgenfunction @@ -313,6 +315,42 @@ async def run(self, test_ctx, contextvars_ctx): raise RuntimeError("too many yields in fixture") +def _trio_test(fn=None, *, run=trio.run): + """Use: + @trio_test + async def test_whatever(): + await ... + + Also: if a pytest fixture is passed in that subclasses the ``Clock`` abc, then + that clock is passed to ``trio.run()``. + """ + + def decorator(fn): + @wraps(fn) + def wrapper(**kwargs): + __tracebackhide__ = True + clocks = [c for c in kwargs.values() if isinstance(c, Clock)] + if not clocks: + clock = None + elif len(clocks) == 1: + clock = clocks[0] + else: + raise ValueError("too many clocks spoil the broth!") + instruments = [ + i for i in kwargs.values() if isinstance(i, Instrument) + ] + return run( + partial(fn, **kwargs), clock=clock, instruments=instruments + ) + + return wrapper + + if fn is None: + return decorator + + return decorator(fn) + + def _trio_test_runner_factory(item, testfunc=None): if testfunc: run = trio.run @@ -345,7 +383,7 @@ def _trio_test_runner_factory(item, testfunc=None): 'test function `%r` is marked trio but is not async' % item ) - @trio_test(run=run) + @_trio_test(run=run) async def _bootstrap_fixtures_and_run_test(**kwargs): __tracebackhide__ = True From a4bb17446789b0ae7d8572a3d99caa7cc173f014 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 27 Aug 2020 18:27:40 -0400 Subject: [PATCH 114/185] Apply trio_run config for @pytest.mark.trio as well --- pytest_trio/_tests/test_trio_mode.py | 40 ++++++++++++++++++++++------ pytest_trio/plugin.py | 27 ++++++++++++------- 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/pytest_trio/_tests/test_trio_mode.py b/pytest_trio/_tests/test_trio_mode.py index 26bba62..56b2092 100644 --- a/pytest_trio/_tests/test_trio_mode.py +++ b/pytest_trio/_tests/test_trio_mode.py @@ -38,29 +38,53 @@ def test_trio_mode(testdir, enable_trio_mode): result.assert_outcomes(passed=2, failed=2) -def test_qtrio_mode_configuration(testdir): +qtrio_text = """ +import trio + +fake_used = False + +def run(*args, **kwargs): + global fake_used + fake_used = True + + return trio.run(*args, **kwargs) +""" + + +def test_qtrio_mode_and_run_configuration(testdir): testdir.makefile( ".ini", pytest="[pytest]\ntrio_mode = true\ntrio_run = qtrio\n" ) - qtrio_text = """ + testdir.makepyfile(qtrio=qtrio_text) + + test_text = """ + import qtrio import trio - fake_used = False + async def test_fake_qtrio_used(): + await trio.sleep(0) + assert qtrio.fake_used + """ + testdir.makepyfile(test_text) - def run(*args, **kwargs): - global fake_used - fake_used = True + result = testdir.runpytest() + result.assert_outcomes(passed=1) - return trio.run(*args, **kwargs) - """ + +def test_qtrio_just_run_configuration(testdir): + testdir.makefile( + ".ini", pytest="[pytest]\ntrio_run = qtrio\n" + ) testdir.makepyfile(qtrio=qtrio_text) test_text = """ + import pytest import qtrio import trio + @pytest.mark.trio async def test_fake_qtrio_used(): await trio.sleep(0) assert qtrio.fake_used diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index a86840e..1e9e3ab 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -131,6 +131,9 @@ def pytest_exception_interact(node, call, report): canary = contextvars.ContextVar("pytest-trio canary") +config_run = None + + class TrioTestContext: def __init__(self): self.crashed = False @@ -358,7 +361,7 @@ def _trio_test_runner_factory(item, testfunc=None): testfunc = item.obj runs = { - marker.kwargs.get('run', trio.run) + marker.kwargs.get('run', config_run) for marker in item.iter_markers("trio") } @@ -512,17 +515,21 @@ def automark(items, run=trio.run): def pytest_collection_modifyitems(config, items): - if config.getini("trio_mode"): - [run_string] = config.getini("trio_run") + global config_run - if run_string == "trio": - run = trio.run - elif run_string == "qtrio": - import qtrio - run = qtrio.run - else: - 1 / 0 + [run_string] = config.getini("trio_run") + if run_string == "trio": + run = trio.run + elif run_string == "qtrio": + import qtrio + run = qtrio.run + else: + 1 / 0 + + config_run = run + + if config.getini("trio_mode"): automark(items, run=run) From cf9288c6d9d43fff062e148aafd2da775214ee4a Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 27 Aug 2020 20:41:00 -0400 Subject: [PATCH 115/185] Drop that global config_run --- pytest_trio/plugin.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 1e9e3ab..80d8545 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -131,9 +131,6 @@ def pytest_exception_interact(node, call, report): canary = contextvars.ContextVar("pytest-trio canary") -config_run = None - - class TrioTestContext: def __init__(self): self.crashed = False @@ -360,6 +357,7 @@ def _trio_test_runner_factory(item, testfunc=None): else: testfunc = item.obj + config_run = choose_run(config=item.config) runs = { marker.kwargs.get('run', config_run) for marker in item.iter_markers("trio") @@ -514,9 +512,7 @@ def automark(items, run=trio.run): item.add_marker(pytest.mark.trio(run=run)) -def pytest_collection_modifyitems(config, items): - global config_run - +def choose_run(config): [run_string] = config.getini("trio_run") if run_string == "trio": @@ -527,10 +523,12 @@ def pytest_collection_modifyitems(config, items): else: 1 / 0 - config_run = run + return run + +def pytest_collection_modifyitems(config, items): if config.getini("trio_mode"): - automark(items, run=run) + automark(items, run=choose_run(config=config)) ################################################################ From a7be06721991625ee735bb6beab64386b377ae66 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 27 Aug 2020 20:41:22 -0400 Subject: [PATCH 116/185] yapf --- pytest_trio/_tests/test_trio_mode.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pytest_trio/_tests/test_trio_mode.py b/pytest_trio/_tests/test_trio_mode.py index 56b2092..bb06cff 100644 --- a/pytest_trio/_tests/test_trio_mode.py +++ b/pytest_trio/_tests/test_trio_mode.py @@ -73,9 +73,7 @@ async def test_fake_qtrio_used(): def test_qtrio_just_run_configuration(testdir): - testdir.makefile( - ".ini", pytest="[pytest]\ntrio_run = qtrio\n" - ) + testdir.makefile(".ini", pytest="[pytest]\ntrio_run = qtrio\n") testdir.makepyfile(qtrio=qtrio_text) From dbcb6dc2d7fc8b5f0bbf758b90ee4c4fd60e8a59 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 27 Aug 2020 21:19:43 -0400 Subject: [PATCH 117/185] Replace 1 / 0 with real exceptions --- pytest_trio/plugin.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 80d8545..6f41228 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -364,7 +364,7 @@ def _trio_test_runner_factory(item, testfunc=None): } if len(runs) == 0: - 1 / 0 + raise RuntimeError("No 'trio' marker found.") elif len(runs) == 1: [run] = runs else: @@ -372,7 +372,10 @@ def _trio_test_runner_factory(item, testfunc=None): if len(runs) == 1: [run] = runs else: - 1 / 0 + raise ValueError( + "Not yet able to select from more than one third-party" + + f" runner. Found: {', '.join(sorted(runs))}" + ) if getattr(testfunc, '_trio_test_runner_wrapped', False): # We have already wrapped this, perhaps because we combined Hypothesis @@ -521,7 +524,10 @@ def choose_run(config): import qtrio run = qtrio.run else: - 1 / 0 + raise ValueError( + f"{run_string!r} not valid for 'trio_run' config." + + " Must be one of: trio, qtrio" + ) return run From 6ace199169b6533bf3fbe74ea18125996317880d Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 27 Aug 2020 21:31:58 -0400 Subject: [PATCH 118/185] yapf --- pytest_trio/plugin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 6f41228..9209de5 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -373,8 +373,8 @@ def _trio_test_runner_factory(item, testfunc=None): [run] = runs else: raise ValueError( - "Not yet able to select from more than one third-party" - + f" runner. Found: {', '.join(sorted(runs))}" + "Not yet able to select from more than one third-party" + + f" runner. Found: {', '.join(sorted(runs))}" ) if getattr(testfunc, '_trio_test_runner_wrapped', False): @@ -525,8 +525,8 @@ def choose_run(config): run = qtrio.run else: raise ValueError( - f"{run_string!r} not valid for 'trio_run' config." - + " Must be one of: trio, qtrio" + f"{run_string!r} not valid for 'trio_run' config." + + " Must be one of: trio, qtrio" ) return run From 0c01e6fa5acb7faa6c0ca2a18ba82b38c4e3f732 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 27 Aug 2020 21:42:49 -0400 Subject: [PATCH 119/185] requiring calling as @_trio_test(run=something) --- pytest_trio/plugin.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 9209de5..f51d5b0 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -315,7 +315,7 @@ async def run(self, test_ctx, contextvars_ctx): raise RuntimeError("too many yields in fixture") -def _trio_test(fn=None, *, run=trio.run): +def _trio_test(run): """Use: @trio_test async def test_whatever(): @@ -345,10 +345,7 @@ def wrapper(**kwargs): return wrapper - if fn is None: - return decorator - - return decorator(fn) + return decorator def _trio_test_runner_factory(item, testfunc=None): From 2be7a24b2737dd6af440cd172f4c8e5815f2ead4 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 27 Aug 2020 22:05:23 -0400 Subject: [PATCH 120/185] add test for invalid trio_run name --- pytest_trio/_tests/test_trio_mode.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pytest_trio/_tests/test_trio_mode.py b/pytest_trio/_tests/test_trio_mode.py index bb06cff..58df7a8 100644 --- a/pytest_trio/_tests/test_trio_mode.py +++ b/pytest_trio/_tests/test_trio_mode.py @@ -91,3 +91,23 @@ async def test_fake_qtrio_used(): result = testdir.runpytest() result.assert_outcomes(passed=1) + + +def test_invalid_trio_run_fails(testdir): + run_name = "invalid_trio_run" + + testdir.makefile( + ".ini", pytest=f"[pytest]\ntrio_mode = true\ntrio_run = {run_name}\n" + ) + + test_text = """ + async def test(): + pass + """ + testdir.makepyfile(test_text) + + result = testdir.runpytest() + result.assert_outcomes() + result.stdout.fnmatch_lines([ + f"*ValueError: {run_name!r} not valid for 'trio_run' config. Must be one of: *" + ]) From e9335c70e350954f0dea8bf71a39bf015674163a Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 27 Aug 2020 22:40:15 -0400 Subject: [PATCH 121/185] add test for multiple third party runners error --- pytest_trio/_tests/test_trio_mode.py | 25 +++++++++++++++++++++++++ pytest_trio/plugin.py | 3 ++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/pytest_trio/_tests/test_trio_mode.py b/pytest_trio/_tests/test_trio_mode.py index 58df7a8..2df0724 100644 --- a/pytest_trio/_tests/test_trio_mode.py +++ b/pytest_trio/_tests/test_trio_mode.py @@ -111,3 +111,28 @@ async def test(): result.stdout.fnmatch_lines([ f"*ValueError: {run_name!r} not valid for 'trio_run' config. Must be one of: *" ]) + + +def test_multiple_custom_trio_runs_fail(testdir): + test_text = """ + import pytest + import pytest_trio + + def f(): + pass + + def g(): + pass + + @pytest.mark.trio(run=f) + @pytest.mark.trio(run=g) + async def test(): + pass + """ + testdir.makepyfile(test_text) + + result = testdir.runpytest() + result.assert_outcomes(failed=1) + result.stdout.fnmatch_lines([ + f"*ValueError: Not yet able to select from more than one third-party runner. Found: *" + ]) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index f51d5b0..27d3d64 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -369,9 +369,10 @@ def _trio_test_runner_factory(item, testfunc=None): if len(runs) == 1: [run] = runs else: + runs_string = ', '.join(sorted(f"{f.__module__}.{f.__name__}" for f in runs)) raise ValueError( "Not yet able to select from more than one third-party" + - f" runner. Found: {', '.join(sorted(runs))}" + f" runner. Found: {runs_string}" ) if getattr(testfunc, '_trio_test_runner_wrapped', False): From 1639e347c88233cce38f00426941348b51537243 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 27 Aug 2020 23:30:15 -0400 Subject: [PATCH 122/185] add test for qtrio.run marker over trio mode --- pytest_trio/_tests/test_trio_mode.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/pytest_trio/_tests/test_trio_mode.py b/pytest_trio/_tests/test_trio_mode.py index 2df0724..25d692d 100644 --- a/pytest_trio/_tests/test_trio_mode.py +++ b/pytest_trio/_tests/test_trio_mode.py @@ -51,7 +51,7 @@ def run(*args, **kwargs): """ -def test_qtrio_mode_and_run_configuration(testdir): +def test_trio_mode_and_qtrio_run_configuration(testdir): testdir.makefile( ".ini", pytest="[pytest]\ntrio_mode = true\ntrio_run = qtrio\n" ) @@ -72,6 +72,27 @@ async def test_fake_qtrio_used(): result.assert_outcomes(passed=1) +def test_trio_mode_and_qtrio_marker(testdir): + testdir.makefile(".ini", pytest="[pytest]\ntrio_mode = true\n") + + testdir.makepyfile(qtrio=qtrio_text) + + test_text = """ + import pytest + import qtrio + import trio + + @pytest.mark.trio(run=qtrio.run) + async def test_fake_qtrio_used(): + await trio.sleep(0) + assert qtrio.fake_used + """ + testdir.makepyfile(test_text) + + result = testdir.runpytest() + result.assert_outcomes(passed=1) + + def test_qtrio_just_run_configuration(testdir): testdir.makefile(".ini", pytest="[pytest]\ntrio_run = qtrio\n") From a2cf6e6f7b37d042e24fddcf1b8f22d763febe08 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 27 Aug 2020 23:36:10 -0400 Subject: [PATCH 123/185] yapf --- pytest_trio/_tests/test_trio_mode.py | 16 ++++++++++------ pytest_trio/plugin.py | 4 +++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/pytest_trio/_tests/test_trio_mode.py b/pytest_trio/_tests/test_trio_mode.py index 25d692d..bed42d6 100644 --- a/pytest_trio/_tests/test_trio_mode.py +++ b/pytest_trio/_tests/test_trio_mode.py @@ -129,9 +129,11 @@ async def test(): result = testdir.runpytest() result.assert_outcomes() - result.stdout.fnmatch_lines([ - f"*ValueError: {run_name!r} not valid for 'trio_run' config. Must be one of: *" - ]) + result.stdout.fnmatch_lines( + [ + f"*ValueError: {run_name!r} not valid for 'trio_run' config. Must be one of: *" + ] + ) def test_multiple_custom_trio_runs_fail(testdir): @@ -154,6 +156,8 @@ async def test(): result = testdir.runpytest() result.assert_outcomes(failed=1) - result.stdout.fnmatch_lines([ - f"*ValueError: Not yet able to select from more than one third-party runner. Found: *" - ]) + result.stdout.fnmatch_lines( + [ + f"*ValueError: Not yet able to select from more than one third-party runner. Found: *" + ] + ) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 27d3d64..987ae3e 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -369,7 +369,9 @@ def _trio_test_runner_factory(item, testfunc=None): if len(runs) == 1: [run] = runs else: - runs_string = ', '.join(sorted(f"{f.__module__}.{f.__name__}" for f in runs)) + runs_string = ', '.join( + sorted(f"{f.__module__}.{f.__name__}" for f in runs) + ) raise ValueError( "Not yet able to select from more than one third-party" + f" runner. Found: {runs_string}" From 910de1dd39634cbab61ca09091e94e20ca9d5059 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 28 Aug 2020 11:03:16 -0400 Subject: [PATCH 124/185] more tests and coverage --- pytest_trio/_tests/test_fixture_mistakes.py | 25 +++++++++++++++++++++ pytest_trio/_tests/test_trio_mode.py | 12 +++++----- pytest_trio/plugin.py | 6 +++-- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/pytest_trio/_tests/test_fixture_mistakes.py b/pytest_trio/_tests/test_fixture_mistakes.py index 5521a80..077861d 100644 --- a/pytest_trio/_tests/test_fixture_mistakes.py +++ b/pytest_trio/_tests/test_fixture_mistakes.py @@ -146,3 +146,28 @@ async def test_whatever(async_fixture): result.assert_outcomes(failed=1) result.stdout.fnmatch_lines(["*async_fixture*cancelled the test*"]) + + +@enable_trio_mode +def test_too_many_clocks(testdir, enable_trio_mode): + enable_trio_mode(testdir) + + testdir.makepyfile( + """ + import pytest + + @pytest.fixture + def extra_clock(mock_clock): + return mock_clock + + async def test_whatever(mock_clock, extra_clock): + pass + """ + ) + + result = testdir.runpytest() + + result.assert_outcomes(failed=1) + result.stdout.fnmatch_lines( + ["*ValueError: too many clocks spoil the broth!*"] + ) diff --git a/pytest_trio/_tests/test_trio_mode.py b/pytest_trio/_tests/test_trio_mode.py index bed42d6..7aad25a 100644 --- a/pytest_trio/_tests/test_trio_mode.py +++ b/pytest_trio/_tests/test_trio_mode.py @@ -136,7 +136,13 @@ async def test(): ) -def test_multiple_custom_trio_runs_fail(testdir): +def test_third_party_run_plus_marker_fails(testdir): + testdir.makefile( + ".ini", pytest=f"[pytest]\ntrio_mode = true\ntrio_run = qtrio\n" + ) + + testdir.makepyfile(qtrio=qtrio_text) + test_text = """ import pytest import pytest_trio @@ -144,11 +150,7 @@ def test_multiple_custom_trio_runs_fail(testdir): def f(): pass - def g(): - pass - @pytest.mark.trio(run=f) - @pytest.mark.trio(run=g) async def test(): pass """ diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 987ae3e..f47a7d6 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -360,8 +360,10 @@ def _trio_test_runner_factory(item, testfunc=None): for marker in item.iter_markers("trio") } - if len(runs) == 0: - raise RuntimeError("No 'trio' marker found.") + if len(runs) == 0: # pragma: no cover + raise RuntimeError( + "No 'trio' marker found. Please report to pytest-trio." + ) elif len(runs) == 1: [run] = runs else: From 511a6f6354d32ad651051bc00738d1c6269b0050 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 28 Aug 2020 23:56:38 -0400 Subject: [PATCH 125/185] #egg=trio --- test-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index 9139fcf..a5a6f77 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,4 @@ pytest >= 6.0.0 # https://github.com/python-trio/pytest-trio/pull/98#issuecomment-678699693 pytest-cov hypothesis>=3.64 -https://github.com/altendky/trio/archive/configurable_trio_run.zip +https://github.com/altendky/trio/archive/configurable_trio_run.zip#egg=trio From 571128be63984a4fc19b19093aa693b37ea2ef7b Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 29 Aug 2020 11:32:50 -0400 Subject: [PATCH 126/185] Add trio_run documentation --- docs/source/reference.rst | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/source/reference.rst b/docs/source/reference.rst index 2018b84..8070bce 100644 --- a/docs/source/reference.rst +++ b/docs/source/reference.rst @@ -381,3 +381,40 @@ write `stateful tests `__ for Trio-based libraries, then check out `hypothesis-trio `__. + + +Using alternative Trio runners +------------------------------ + +If you are working with a library that provides integration with Trio, +such as via :ref:`guest mode `, it can be used with +pytest-trio as well. Setting ``trio_run`` in the pytest configuration +makes your choice the global default for both tests explicitly marked +with ``@pytest.mark.trio`` and those automatically marked by Trio mode. +``trio_run`` presently supports ``trio`` and ``qtrio``. + +.. code-block:: ini + + # pytest.ini + [pytest] + trio_mode = true + trio_run = qtrio + +.. code-block:: python + + import pytest + + @pytest.mark.trio + async def test(): + assert True + +If you want more granular control or need to use a specific function, +it can be passed directly to the marker. + +.. code-block:: python + + import pytest + + @pytest.mark.trio(run=qtrio.run) + async def test(): + assert True From 339b34b9cba4eb7d43efffd500af6e2d69430be5 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 29 Aug 2020 16:45:08 -0400 Subject: [PATCH 127/185] Drop no longer needed Trio PR dependency --- test-requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/test-requirements.txt b/test-requirements.txt index a5a6f77..5c53c25 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,4 +1,3 @@ pytest >= 6.0.0 # https://github.com/python-trio/pytest-trio/pull/98#issuecomment-678699693 pytest-cov hypothesis>=3.64 -https://github.com/altendky/trio/archive/configurable_trio_run.zip#egg=trio From ad87be94b82f387695ffcd6d3c2b4e2149be4e09 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 29 Aug 2020 17:49:43 -0400 Subject: [PATCH 128/185] Add trio-run-config anchor in the docs --- docs/source/reference.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/reference.rst b/docs/source/reference.rst index 8070bce..351e8c1 100644 --- a/docs/source/reference.rst +++ b/docs/source/reference.rst @@ -383,6 +383,8 @@ Trio-based libraries, then check out `hypothesis-trio `__. +.. _trio-run-config: + Using alternative Trio runners ------------------------------ From a1f72188d1bfa39b2c67194e61800245d92674c4 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 2 Sep 2020 10:07:47 -0400 Subject: [PATCH 129/185] Switch back to end_time for consistency --- docs/source/quickstart.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index adfe64d..0ef3164 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -133,8 +133,8 @@ something will happen:: async def test_sleep_efficiently_and_reliably(autojump_clock): start_time = trio.current_time() await trio.sleep(1) - finish_time = trio.current_time() - assert finish_time - start_time == 1 + end_time = trio.current_time() + assert end_time - start_time == 1 In the version of this test we saw before that used real time, at the end we had to use a ``>=`` comparison, in order to account for From e72dd68cf1294fa6f6ec0abff948d24c8b0c6edc Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 10 Sep 2020 22:55:02 -0400 Subject: [PATCH 130/185] Add 3.9-dev to Travis matrix https://github.com/python-trio/pytest-trio/issues/70 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index c5fd788..a52c6ce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ python: - 3.7-dev - 3.8 - 3.8-dev +- 3.9-dev jobs: include: - os: osx From f2ab69293ce54a6b703a554b997576f965c1f9cc Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 18 Sep 2020 22:33:00 -0400 Subject: [PATCH 131/185] Use f.__qualname__ when reporting too many runners --- pytest_trio/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index f47a7d6..17a5b9a 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -372,7 +372,7 @@ def _trio_test_runner_factory(item, testfunc=None): [run] = runs else: runs_string = ', '.join( - sorted(f"{f.__module__}.{f.__name__}" for f in runs) + sorted(f"{f.__module__}.{f.__qualname__}" for f in runs) ) raise ValueError( "Not yet able to select from more than one third-party" + From a45d3149f98ec29d81841c67c355c7790799627b Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 2 Oct 2020 10:50:15 -0400 Subject: [PATCH 132/185] Explain reason for fake qtrio module-in-a-string --- pytest_trio/_tests/test_trio_mode.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pytest_trio/_tests/test_trio_mode.py b/pytest_trio/_tests/test_trio_mode.py index 7aad25a..2c182e3 100644 --- a/pytest_trio/_tests/test_trio_mode.py +++ b/pytest_trio/_tests/test_trio_mode.py @@ -38,6 +38,10 @@ def test_trio_mode(testdir, enable_trio_mode): result.assert_outcomes(passed=2, failed=2) +# This is faking qtrio due to real qtrio's dependence on either +# PyQt5 or PySide2. They are both large and require special +# handling in CI. The testing here is able to focus on the +# pytest-trio features with just this minimal substitute. qtrio_text = """ import trio From 7f7c4e13a54cfa09d7b84b996e96a9c2bb16cdc2 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 2 Oct 2020 11:19:13 -0400 Subject: [PATCH 133/185] Make trio_run a regular string ini option --- pytest_trio/plugin.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 17a5b9a..e28c18e 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -44,8 +44,7 @@ def pytest_addoption(parser): parser.addini( "trio_run", "what runner should pytest-trio use? [trio, qtrio]", - type="linelist", - default=["trio"], + default="trio", ) @@ -518,7 +517,7 @@ def automark(items, run=trio.run): def choose_run(config): - [run_string] = config.getini("trio_run") + run_string = config.getini("trio_run") if run_string == "trio": run = trio.run From c998963007df7ed49f1f927fe6e68854e4b28c87 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 2 Oct 2020 11:25:56 -0400 Subject: [PATCH 134/185] Tweak string split across lines --- pytest_trio/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index e28c18e..46775d0 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -374,7 +374,7 @@ def _trio_test_runner_factory(item, testfunc=None): sorted(f"{f.__module__}.{f.__qualname__}" for f in runs) ) raise ValueError( - "Not yet able to select from more than one third-party" + + f"Not yet able to select from more than one third-party" f" runner. Found: {runs_string}" ) From 68860e0cc703856e3dfa5600f5a04b39e2422b0b Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 2 Oct 2020 17:27:25 -0400 Subject: [PATCH 135/185] Remove checks for multiple conflicting runner specification --- pytest_trio/_tests/test_trio_mode.py | 29 ---------------------------- pytest_trio/plugin.py | 25 ++---------------------- 2 files changed, 2 insertions(+), 52 deletions(-) diff --git a/pytest_trio/_tests/test_trio_mode.py b/pytest_trio/_tests/test_trio_mode.py index 2c182e3..6b31a8a 100644 --- a/pytest_trio/_tests/test_trio_mode.py +++ b/pytest_trio/_tests/test_trio_mode.py @@ -138,32 +138,3 @@ async def test(): f"*ValueError: {run_name!r} not valid for 'trio_run' config. Must be one of: *" ] ) - - -def test_third_party_run_plus_marker_fails(testdir): - testdir.makefile( - ".ini", pytest=f"[pytest]\ntrio_mode = true\ntrio_run = qtrio\n" - ) - - testdir.makepyfile(qtrio=qtrio_text) - - test_text = """ - import pytest - import pytest_trio - - def f(): - pass - - @pytest.mark.trio(run=f) - async def test(): - pass - """ - testdir.makepyfile(test_text) - - result = testdir.runpytest() - result.assert_outcomes(failed=1) - result.stdout.fnmatch_lines( - [ - f"*ValueError: Not yet able to select from more than one third-party runner. Found: *" - ] - ) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 46775d0..2b0c712 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -354,29 +354,8 @@ def _trio_test_runner_factory(item, testfunc=None): testfunc = item.obj config_run = choose_run(config=item.config) - runs = { - marker.kwargs.get('run', config_run) - for marker in item.iter_markers("trio") - } - - if len(runs) == 0: # pragma: no cover - raise RuntimeError( - "No 'trio' marker found. Please report to pytest-trio." - ) - elif len(runs) == 1: - [run] = runs - else: - runs.discard(trio.run) - if len(runs) == 1: - [run] = runs - else: - runs_string = ', '.join( - sorted(f"{f.__module__}.{f.__qualname__}" for f in runs) - ) - raise ValueError( - f"Not yet able to select from more than one third-party" - f" runner. Found: {runs_string}" - ) + marker = item.get_closest_marker("trio") + run = marker.kwargs.get('run', config_run) if getattr(testfunc, '_trio_test_runner_wrapped', False): # We have already wrapped this, perhaps because we combined Hypothesis From 2b89fc1cca891c9862c7f99d60e26ceb23184fb9 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 10 Oct 2020 22:58:23 -0400 Subject: [PATCH 136/185] Use closest run-specifying marker, else config --- pytest_trio/_tests/test_trio_mode.py | 43 ++++++++++++++++++++++++++++ pytest_trio/plugin.py | 12 ++++++-- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/pytest_trio/_tests/test_trio_mode.py b/pytest_trio/_tests/test_trio_mode.py index 6b31a8a..f7bf61f 100644 --- a/pytest_trio/_tests/test_trio_mode.py +++ b/pytest_trio/_tests/test_trio_mode.py @@ -138,3 +138,46 @@ async def test(): f"*ValueError: {run_name!r} not valid for 'trio_run' config. Must be one of: *" ] ) + + +def test_closest_explicit_run_wins(testdir): + testdir.makefile( + ".ini", pytest=f"[pytest]\ntrio_mode = true\ntrio_run = trio\n" + ) + testdir.makepyfile(qtrio=qtrio_text) + + test_text = """ + import pytest + import pytest_trio + import qtrio + + @pytest.mark.trio(run='should be ignored') + @pytest.mark.trio(run=qtrio.run) + async def test(): + assert qtrio.fake_used + """ + testdir.makepyfile(test_text) + + result = testdir.runpytest() + result.assert_outcomes(passed=1) + + +def test_ini_run_wins_with_blank_marker(testdir): + testdir.makefile( + ".ini", pytest=f"[pytest]\ntrio_mode = true\ntrio_run = qtrio\n" + ) + testdir.makepyfile(qtrio=qtrio_text) + + test_text = """ + import pytest + import pytest_trio + import qtrio + + @pytest.mark.trio + async def test(): + assert qtrio.fake_used + """ + testdir.makepyfile(test_text) + + result = testdir.runpytest() + result.assert_outcomes(passed=1) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 2b0c712..e5cd9ab 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -353,9 +353,15 @@ def _trio_test_runner_factory(item, testfunc=None): else: testfunc = item.obj - config_run = choose_run(config=item.config) - marker = item.get_closest_marker("trio") - run = marker.kwargs.get('run', config_run) + for marker in item.iter_markers("trio"): + maybe_run = marker.kwargs.get('run') + if maybe_run is not None: + run = maybe_run + break + else: + # no marker found that explicitly specifiers the runner so use config + run = choose_run(config=item.config) + if getattr(testfunc, '_trio_test_runner_wrapped', False): # We have already wrapped this, perhaps because we combined Hypothesis From 682499fd440f37055de0fc901a79354dc028fd09 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 10 Oct 2020 23:09:58 -0400 Subject: [PATCH 137/185] remove blank line for yapf --- pytest_trio/plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index e5cd9ab..61c4181 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -362,7 +362,6 @@ def _trio_test_runner_factory(item, testfunc=None): # no marker found that explicitly specifiers the runner so use config run = choose_run(config=item.config) - if getattr(testfunc, '_trio_test_runner_wrapped', False): # We have already wrapped this, perhaps because we combined Hypothesis # with pytest.mark.parametrize From 9f610c1343f44f50e0dd51683add27474f9e343f Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 13 Oct 2020 19:40:48 -0400 Subject: [PATCH 138/185] Add missing newsfragments for trio_run and py35 removal --- newsfragments/105.feature.rst | 1 + newsfragments/96.removal.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 newsfragments/105.feature.rst create mode 100644 newsfragments/96.removal.rst diff --git a/newsfragments/105.feature.rst b/newsfragments/105.feature.rst new file mode 100644 index 0000000..4b510a3 --- /dev/null +++ b/newsfragments/105.feature.rst @@ -0,0 +1 @@ +Support added for :ref:`alternative Trio run functions ` via the ``trio_run`` configuration variable and ``@pytest.mark.trio(run=...)``. Presently supports Trio and QTrio. diff --git a/newsfragments/96.removal.rst b/newsfragments/96.removal.rst new file mode 100644 index 0000000..8051806 --- /dev/null +++ b/newsfragments/96.removal.rst @@ -0,0 +1 @@ +Python 3.5 support removed. From 9a669a4ea89ca4292425ed195ffbaecdc9d84931 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 13 Oct 2020 21:25:59 -0400 Subject: [PATCH 139/185] Align more closely with Trio RTD configuration --- .readthedocs.yml | 14 +- ...-requirements.txt => docs-requirements.txt | 0 docs/source/_static/favicon-32.ico | Bin 0 -> 4286 bytes docs/source/_static/favicon-32.png | Bin 0 -> 1834 bytes docs/source/_static/favicon.svg | 177 ++++++++++++++++++ docs/source/conf.py | 33 +++- 6 files changed, 215 insertions(+), 9 deletions(-) rename ci/rtd-requirements.txt => docs-requirements.txt (100%) create mode 100644 docs/source/_static/favicon-32.ico create mode 100644 docs/source/_static/favicon-32.png create mode 100644 docs/source/_static/favicon.svg diff --git a/.readthedocs.yml b/.readthedocs.yml index da6abdf..2a32d6c 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,10 +1,14 @@ -# https://docs.readthedocs.io/en/latest/yaml-config.html +# https://docs.readthedocs.io/en/latest/config-file/index.html +version: 2 + formats: - htmlzip - epub -requirements_file: ci/rtd-requirements.txt - python: - version: 3 - pip_install: True + version: 3.7 + install: + - requirements: docs-requirements.txt + +sphinx: + fail_on_warning: true diff --git a/ci/rtd-requirements.txt b/docs-requirements.txt similarity index 100% rename from ci/rtd-requirements.txt rename to docs-requirements.txt diff --git a/docs/source/_static/favicon-32.ico b/docs/source/_static/favicon-32.ico new file mode 100644 index 0000000000000000000000000000000000000000..1ec20bf8a49972877dd2a5651dd8f0f28680c656 GIT binary patch literal 4286 zcmcIndu&r>6u)EY5`#h_F=*n31VzQTwzu1EyGOgIQDOv*A}R?IAh2%h*4oEj-KYi( z;bkO*wQL=Gv}@Nr5geKzS?~}4LGTaB*nD9$5*6l1#@PGwckb=y?w%O|ww!+V@qOoa zzH@%(obL)k9R63V5a?HkEER-D1wnWQf=7ZGLeJ$tygRf=5#_B`3V*RrdyIrq*y-))Uc^Ox9eQ$?88|cWtk~;%bY>G}=~{b*8mEJALMgg*|Do zNxGmTt+l}+T?;y;k)YGi+vqa11zm=%jSllNt|jU+FOL+bv!>zs-*A_V9GhT3ae3JLa+_Z$58y8(A07( zXldqaWCNb1VQO2NiZV{MmAv=}e9}*L(L2Nv>tguWBlg>0nXB1yEKkd-@^lQgFtshRPhihv zeBhhe;{(o04Dn&GMcym$uD6tC+KC$+pWYL%>gFYWNjHFbOz$dx|i!xzL$;s4$L1>+cK3aN_^xza!3*2UnP9XSB?+<+~D{M2gFNWulYXMaGu*x znWKGIjjz7;+r-DJ3&av|rF$N(^flNGtAKx#YL$#HbI$=9OeU5CYw(2AB$;FyN@zQZyYJIg`C)ebe_~4*JIy(&?yw@fZ#<9HQr-AufVxWo_QDA}bJnX{BTls8K5-U`@K zHgR1o<>r$+I!^sdb=vFSJwo`6PQxeDa{y~A_!>U?^0ZmLZMs*1y9bz262B$61>^T* zgMuB)Szb4kdNJTH5|@x3?B5;rWcM9DxU(l*oH?p+P_j(V0g4~TRgeB0ZL#{EU;Fbk zE;&!JZ>m{x9>;2xT)Ul88qIAl&lbS}xi`depbPbCJ=M@@Inc{>^RY7Lh39=an&0`o zdJI_ftEo;+#h&aNAy?W{d_M47kOQ|OB^gIgxYHjByV5gxANaoxqYecM^{e?B>T~e% zS;!2#Xik+?zi%dXEcYE!H`g=U7%I+KDc50DA0Yo>Kl}N7_}xM=_T?m>;m`PpSnWjK zPsqKB`V{Ih`I727_BGMylwRj^XB+8!0Q~V8Ibanh{JFXYU#>0)>@gYNqTS1NkWawV zB4CeG-J-h3`!wkypAxtE{Q1U^+t?%HQ!nN@2p&-nB3>)l*gtZB^iUqw6o{c2Jd^h= zpHa|rqyuE*e7O7_`as?E9H4ko9&>!kf3H_qRF$9lBw}-M5_^(o%DxT5A-5r)>t4u+ z^VCMpL5iQ;2fzWsr(R6^uguYGsAOE?WqudJgq7;Quwf z4pUAMK59;9pg{K~um|Vj8GPuEdd-W~dT-|%wr`tq7NHzK4E%YzpR26e&A=X;gJ<2S zqq$+Xp*37=>_Ttf4t~gI$=kuFx%Ey%2{=5a;^6tpe7za59+(AtH0Us#M65f3KS5_Y zs%dn-L466|E;gSZvZt)9v!!ki*z{|o8#crPyLmF!Jh!DmIXzNwtUr|?WZ(-AhE<=hQN2dJmnn(D`ajqjiA^>>2H*HZo> zA8x7YnR1>4E#ui`-kTUUxUI8y(^@^vV5*bvLl- z`)&~X{tn;XyIab$*DMr^u^|&<{YzMvCZ2hPxGRt@EcU7u(37|!2#XVzUP%y^3-LlC z>UfeTPDu2`c^7-)hZYyd4+&cm#so7&NP?IkyC!fA&Lly166=@6#D;`j5XQ#h*?$LB Bvd91c literal 0 HcmV?d00001 diff --git a/docs/source/_static/favicon-32.png b/docs/source/_static/favicon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..73a394c3c3496cc4229cfca564ac202c8f39b280 GIT binary patch literal 1834 zcmV+_2i5qAP)tWm2GSphK;9%8q~Z7H-ML<Ph z=iJ}dKW666oncTQoaA@UId{(dzVG)um$=GNI&)o78^@>MIEG{okS!z*)4A;)vA%ZR zJ?~tO9$pRjpgHS1TM+DnV|S8Bhy%hk&C#ueAD_FcYQo7c(&es(Ks;z&BB;A0q{X7* zO!i}od-cPf`NNQFOhA;`_cOs00tvE&5J5v(r(>-#HfNtT4%xK zZzWj}Nj#8@6MP*WJ%Xc1GLoD%dlroUQMUds48ed|)sL9C0DuQ5^}5vLUJevYl`KGJ z9{7UB$n;eJkDIXBV7n!IwBht859Y$SqzC#|ZHN_~Z~0JWH%da%ddLKmK^AOxd9jxq zPz|zej&c;qm+}kpX6~w*aQ;#_m-Im4g_a)xxG}K{t%t%jM_WrvJClusbS9mMVRBNY z$<60m>j%%QF1=y`1j`|X_=3W1xS*@5zy8ENXLh^H4qjZE2FZE^)cyf;svo~H0z_Gc z&8df?r&QAaf1L*Z?Kh8pfR^TXHp1v(#rjoR=A*vwz z9ieIf0RX^I4ghtlD;nySK77AqbCpP#9F59m4w$um{+AN?yx?pX050wFa9&csqT+%2 z6_4FdaXJ8)@Xd(dRJUx>LP%33Q)YWk5`oPvo3-w{83cw_ZrU{DCmS0FKV7q8(3~xW z3S1XuASc%JxF)sx$I8ho0o0P{mTka+Ws_Dxwm73hMv@bXFA50^f9ADP0OKIFfjC8I z_>*KEk(F=~-aqi`N8XECYtj=C+UD{i;_l^KVMqvie`|Ji4*+mxc9%yx+o$N{5yIjC zh8fX0yYvXvXJh^#sMP zN}3%>{zKUy{im%Q$|fDWb;^tL6L$b0&=D$ush^nWbV}S600@E*+=W)J>0JQuW$d}* zc&~!?v1a`JWVa-qbxq=iPY8!oz@?K{6$QSQ%Xpjf(C@t8QrkYB{{&#O3EU8RP8l_u z8ck$SQMa(-vjhR?=DL&J@(z4-GbD>a0)#H%pHq~`BLL8E+UlNx*)|AYlWbJo>M0FL z#m|*YU%Ip~?vDh$a!GdxD29{|;55KvfZl=&UODhW#qksy4z1jDK~lR^ueU_6e-CE8 zB6onPDP9PmDDbo&Koam%NtiZ>_0L231+Kue!oRSfeUg7_e*-hcyLq5+ZwfIdF* z#tfLT_6{FFJ%Dm$*8Fwh3Id6@K0Q@D15jyF3qTY^LHky1h_yL=t{lWeG?oL*gG;0x z$i%f9nE`YV^$TCZ4guvC_4-H!KyEK!zakv~f&>J&sC3$zE^=%nG?1TsJ_OEP52irV z;1SEiIYEZs_G5CdHY0czz*o{XM=F5$EFNz%I`4+4ViFwz{>(91UQE_pl3@5Q4cZDa zFOwYPcK-5*XJmRRWqmaE5J1x!=loHQi8wMcJLeJ~A*q=Tj;S-@Z-*S+5jy8HONXQP z*YIi_IH#sk3aTXesfvJMZ_wC068U*MNtGeXwUKycSxb<31LXSzCW0)AdM5Ouo^XG09>#;NndCIk6{M(FgR$km={)p!=4tu`QfBLE^m}Qx%M`BtRyrH + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/conf.py b/docs/source/conf.py index 5e508ef..671837f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -22,6 +22,18 @@ # So autodoc can import our package sys.path.insert(0, os.path.abspath('../..')) +# https://docs.readthedocs.io/en/stable/builds.html#build-environment +if "READTHEDOCS" in os.environ: + import glob + if glob.glob("../../newsfragments/*.*.rst"): + print("-- Found newsfragments; running towncrier --", flush=True) + import subprocess + subprocess.run( + ["towncrier", "--yes", "--date", "not released yet"], + cwd="../..", + check=True, + ) + # Warn about all references to unknown targets nitpicky = True # Except for these ones, which we expect to point to unknown targets: @@ -81,6 +93,15 @@ # The full version, including alpha/beta/rc tags. release = version +# It would be nicer to make this a .png; literally every browser that +# supports favicons at all now supports png: +# https://caniuse.com/#feat=link-icon-png +# But sphinx won't let me: +# https://github.com/sphinx-doc/sphinx/pull/3715 +# Oh well. 'convert favicon-32.png favicon-32.ico' it is. And it's only 2x +# bigger... +html_favicon = "_static/favicon-32.ico" + # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # @@ -94,14 +115,17 @@ exclude_patterns = [] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = 'default' -# The default language for :: blocks highlight_language = 'python3' # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False +# This avoids a warning by the epub builder that it can't figure out +# the MIME type for our favicon. +suppress_warnings = ["epub.unknown_project_files"] + # -- Options for HTML output ---------------------------------------------- @@ -129,6 +153,7 @@ # versions/settings... "navigation_depth": 4, "logo_only": True, + 'prev_next_buttons_location': 'both' } # Add any paths that contain custom static files (such as style sheets) here, @@ -167,7 +192,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'pytest-trio.tex', 'Trio Documentation', + (master_doc, 'pytest-trio.tex', 'pytest-trio Documentation', author, 'manual'), ] @@ -189,6 +214,6 @@ # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'pytest-trio', 'pytest-trio Documentation', - author, 'pytest-trio', 'Pytest plugin for trio', + author, 'pytest-trio', 'pytest plugin for Trio', 'Miscellaneous'), ] From 05ba36374b8e997dbc4083cc62006b5657b580f5 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 13 Oct 2020 21:47:40 -0400 Subject: [PATCH 140/185] Update ci/travis.sh to docs-requirements.txt --- ci/travis.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/travis.sh b/ci/travis.sh index cc4b0cd..5b5f857 100755 --- a/ci/travis.sh +++ b/ci/travis.sh @@ -43,7 +43,7 @@ python setup.py sdist --formats=zip pip install dist/*.zip if [ "$CHECK_DOCS" = "1" ]; then - pip install -Ur ci/rtd-requirements.txt + pip install -Ur docs-requirements.txt cd docs # -n (nit-picky): warn on missing references # -W: turn warnings into errors From 646b54fe84c1e085b0d1a1c55093b844f63080c0 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 13 Oct 2020 23:54:49 -0400 Subject: [PATCH 141/185] Add towncrier to docs-requirements.txt --- docs-requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs-requirements.txt b/docs-requirements.txt index ea59c6b..1b3aa0f 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -5,3 +5,5 @@ sphinxcontrib-trio # Workaround for this weird issue: # https://travis-ci.org/python-trio/pytest-trio/jobs/407495415 attrs >= 17.4.0 +# != 19.9.0rc1 for https://github.com/twisted/towncrier/issues/180 +towncrier != 19.9.0rc1 From 079936a540b92a75c5d3b6554cb88abd71a7ae5a Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 14 Oct 2020 00:17:50 -0400 Subject: [PATCH 142/185] docs-requirements.in/txt --- docs-requirements.in | 16 ++++++++++++ docs-requirements.txt | 58 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 docs-requirements.in diff --git a/docs-requirements.in b/docs-requirements.in new file mode 100644 index 0000000..f86bf78 --- /dev/null +++ b/docs-requirements.in @@ -0,0 +1,16 @@ +# RTD is currently installing 1.5.3, which has a bug in :lineno-match: +sphinx >= 1.6.1 +sphinx_rtd_theme +sphinxcontrib-trio +# Workaround for this weird issue: +# https://travis-ci.org/python-trio/pytest-trio/jobs/407495415 +attrs >= 17.4.0 +# != 19.9.0rc1 for https://github.com/twisted/towncrier/issues/180 +towncrier != 19.9.0rc1 + +# pytest-trio's own dependencies +trio >= 0.15.0 +async_generator >= 1.9 +outcome +# For node.get_closest_marker +pytest >= 3.6 diff --git a/docs-requirements.txt b/docs-requirements.txt index 1b3aa0f..5b3f91a 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -1,9 +1,49 @@ -# RTD is currently installing 1.5.3, which has a bug in :lineno-match: -sphinx >= 1.6.1 -sphinx_rtd_theme -sphinxcontrib-trio -# Workaround for this weird issue: -# https://travis-ci.org/python-trio/pytest-trio/jobs/407495415 -attrs >= 17.4.0 -# != 19.9.0rc1 for https://github.com/twisted/towncrier/issues/180 -towncrier != 19.9.0rc1 +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --output-file=docs-requirements.txt docs-requirements.in +# +alabaster==0.7.12 # via sphinx +async-generator==1.10 # via -r docs-requirements.in, trio +attrs==20.2.0 # via -r docs-requirements.in, outcome, pytest, trio +babel==2.8.0 # via sphinx +certifi==2020.6.20 # via requests +chardet==3.0.4 # via requests +click==7.1.2 # via towncrier +docutils==0.16 # via sphinx +idna==2.10 # via requests, trio +imagesize==1.2.0 # via sphinx +incremental==17.5.0 # via towncrier +iniconfig==1.0.1 # via pytest +jinja2==2.11.2 # via sphinx, towncrier +markupsafe==1.1.1 # via jinja2 +outcome==1.0.1 # via -r docs-requirements.in, trio +packaging==20.4 # via pytest, sphinx +pluggy==0.13.1 # via pytest +py==1.9.0 # via pytest +pygments==2.7.1 # via sphinx +pyparsing==2.4.7 # via packaging +pytest==6.1.1 # via -r docs-requirements.in +pytz==2020.1 # via babel +requests==2.24.0 # via sphinx +six==1.15.0 # via packaging +sniffio==1.2.0 # via trio +snowballstemmer==2.0.0 # via sphinx +sortedcontainers==2.2.2 # via trio +sphinx-rtd-theme==0.5.0 # via -r docs-requirements.in +sphinx==3.2.1 # via -r docs-requirements.in, sphinx-rtd-theme, sphinxcontrib-trio +sphinxcontrib-applehelp==1.0.2 # via sphinx +sphinxcontrib-devhelp==1.0.2 # via sphinx +sphinxcontrib-htmlhelp==1.0.3 # via sphinx +sphinxcontrib-jsmath==1.0.1 # via sphinx +sphinxcontrib-qthelp==1.0.3 # via sphinx +sphinxcontrib-serializinghtml==1.1.4 # via sphinx +sphinxcontrib-trio==1.1.2 # via -r docs-requirements.in +toml==0.10.1 # via pytest, towncrier +towncrier==19.2.0 # via -r docs-requirements.in +trio==0.17.0 # via -r docs-requirements.in +urllib3==1.25.10 # via requests + +# The following packages are considered to be unsafe in a requirements file: +# setuptools From 6848012c21bc688b7edca9bdab6be31b2aff987a Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 14 Oct 2020 09:39:34 -0400 Subject: [PATCH 143/185] Use .png for favicon --- docs/source/_static/favicon-32.ico | Bin 4286 -> 0 bytes docs/source/conf.py | 9 +-------- 2 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 docs/source/_static/favicon-32.ico diff --git a/docs/source/_static/favicon-32.ico b/docs/source/_static/favicon-32.ico deleted file mode 100644 index 1ec20bf8a49972877dd2a5651dd8f0f28680c656..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4286 zcmcIndu&r>6u)EY5`#h_F=*n31VzQTwzu1EyGOgIQDOv*A}R?IAh2%h*4oEj-KYi( z;bkO*wQL=Gv}@Nr5geKzS?~}4LGTaB*nD9$5*6l1#@PGwckb=y?w%O|ww!+V@qOoa zzH@%(obL)k9R63V5a?HkEER-D1wnWQf=7ZGLeJ$tygRf=5#_B`3V*RrdyIrq*y-))Uc^Ox9eQ$?88|cWtk~;%bY>G}=~{b*8mEJALMgg*|Do zNxGmTt+l}+T?;y;k)YGi+vqa11zm=%jSllNt|jU+FOL+bv!>zs-*A_V9GhT3ae3JLa+_Z$58y8(A07( zXldqaWCNb1VQO2NiZV{MmAv=}e9}*L(L2Nv>tguWBlg>0nXB1yEKkd-@^lQgFtshRPhihv zeBhhe;{(o04Dn&GMcym$uD6tC+KC$+pWYL%>gFYWNjHFbOz$dx|i!xzL$;s4$L1>+cK3aN_^xza!3*2UnP9XSB?+<+~D{M2gFNWulYXMaGu*x znWKGIjjz7;+r-DJ3&av|rF$N(^flNGtAKx#YL$#HbI$=9OeU5CYw(2AB$;FyN@zQZyYJIg`C)ebe_~4*JIy(&?yw@fZ#<9HQr-AufVxWo_QDA}bJnX{BTls8K5-U`@K zHgR1o<>r$+I!^sdb=vFSJwo`6PQxeDa{y~A_!>U?^0ZmLZMs*1y9bz262B$61>^T* zgMuB)Szb4kdNJTH5|@x3?B5;rWcM9DxU(l*oH?p+P_j(V0g4~TRgeB0ZL#{EU;Fbk zE;&!JZ>m{x9>;2xT)Ul88qIAl&lbS}xi`depbPbCJ=M@@Inc{>^RY7Lh39=an&0`o zdJI_ftEo;+#h&aNAy?W{d_M47kOQ|OB^gIgxYHjByV5gxANaoxqYecM^{e?B>T~e% zS;!2#Xik+?zi%dXEcYE!H`g=U7%I+KDc50DA0Yo>Kl}N7_}xM=_T?m>;m`PpSnWjK zPsqKB`V{Ih`I727_BGMylwRj^XB+8!0Q~V8Ibanh{JFXYU#>0)>@gYNqTS1NkWawV zB4CeG-J-h3`!wkypAxtE{Q1U^+t?%HQ!nN@2p&-nB3>)l*gtZB^iUqw6o{c2Jd^h= zpHa|rqyuE*e7O7_`as?E9H4ko9&>!kf3H_qRF$9lBw}-M5_^(o%DxT5A-5r)>t4u+ z^VCMpL5iQ;2fzWsr(R6^uguYGsAOE?WqudJgq7;Quwf z4pUAMK59;9pg{K~um|Vj8GPuEdd-W~dT-|%wr`tq7NHzK4E%YzpR26e&A=X;gJ<2S zqq$+Xp*37=>_Ttf4t~gI$=kuFx%Ey%2{=5a;^6tpe7za59+(AtH0Us#M65f3KS5_Y zs%dn-L466|E;gSZvZt)9v!!ki*z{|o8#crPyLmF!Jh!DmIXzNwtUr|?WZ(-AhE<=hQN2dJmnn(D`ajqjiA^>>2H*HZo> zA8x7YnR1>4E#ui`-kTUUxUI8y(^@^vV5*bvLl- z`)&~X{tn;XyIab$*DMr^u^|&<{YzMvCZ2hPxGRt@EcU7u(37|!2#XVzUP%y^3-LlC z>UfeTPDu2`c^7-)hZYyd4+&cm#so7&NP?IkyC!fA&Lly166=@6#D;`j5XQ#h*?$LB Bvd91c diff --git a/docs/source/conf.py b/docs/source/conf.py index 671837f..8ac168d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -93,14 +93,7 @@ # The full version, including alpha/beta/rc tags. release = version -# It would be nicer to make this a .png; literally every browser that -# supports favicons at all now supports png: -# https://caniuse.com/#feat=link-icon-png -# But sphinx won't let me: -# https://github.com/sphinx-doc/sphinx/pull/3715 -# Oh well. 'convert favicon-32.png favicon-32.ico' it is. And it's only 2x -# bigger... -html_favicon = "_static/favicon-32.ico" +html_favicon = "_static/favicon-32.png" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 2e06a6a867e436e6205a98e4eaccb36d8760d809 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 14 Oct 2020 09:45:28 -0400 Subject: [PATCH 144/185] Try out the Trio logo for docs with pytest-trio name --- docs/source/conf.py | 4 +- logo/wordmark-transparent.svg | 136 ++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 logo/wordmark-transparent.svg diff --git a/docs/source/conf.py b/docs/source/conf.py index 8ac168d..203bf40 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -94,6 +94,8 @@ release = version html_favicon = "_static/favicon-32.png" +html_logo = "../../logo/wordmark-transparent.svg" +# & down below in html_theme_options we set logo_only=True # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -145,7 +147,7 @@ # I'm not 100% sure this actually does anything with our current # versions/settings... "navigation_depth": 4, - "logo_only": True, + "logo_only": False, 'prev_next_buttons_location': 'both' } diff --git a/logo/wordmark-transparent.svg b/logo/wordmark-transparent.svg new file mode 100644 index 0000000..d8590e8 --- /dev/null +++ b/logo/wordmark-transparent.svg @@ -0,0 +1,136 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + From bb807317eaed9a3fd2cb5aa0d74f5b1ec529b1a1 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 14 Oct 2020 10:34:35 -0400 Subject: [PATCH 145/185] use a tweaked Trio sphinx template --- docs/source/_templates/layout.html | 25 +++++++++++++++++++++++++ docs/source/conf.py | 3 +-- 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 docs/source/_templates/layout.html diff --git a/docs/source/_templates/layout.html b/docs/source/_templates/layout.html new file mode 100644 index 0000000..f06fb9e --- /dev/null +++ b/docs/source/_templates/layout.html @@ -0,0 +1,25 @@ +{# +https://stackoverflow.com/questions/25243482/how-to-add-sphinx-generated-index-to-the-sidebar-when-using-read-the-docs-theme +#} +{% extends "!layout.html" %} + +{% block sidebartitle %} + {{ project }} + + {%- set nav_version = version %} + {% if READTHEDOCS and current_version %} + {%- set nav_version = current_version %} + {% endif %} + {# don't show the version on RTD if it's the default #} + {% if nav_version != 'latest' %} +
{{ nav_version }}
+ {% endif %} +
+ +{% include "searchbox.html" %} + +

Need help? Live chat, forum, StackOverflow.

+{% endblock %} diff --git a/docs/source/conf.py b/docs/source/conf.py index 203bf40..d762180 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -67,7 +67,7 @@ autodoc_member_order = "bysource" # Add any paths that contain templates here, relative to this directory. -templates_path = [] +templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: @@ -95,7 +95,6 @@ html_favicon = "_static/favicon-32.png" html_logo = "../../logo/wordmark-transparent.svg" -# & down below in html_theme_options we set logo_only=True # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From cc76ebe1ecf718ec89b019c6a625acabcd3b6683 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 14 Oct 2020 10:56:29 -0400 Subject: [PATCH 146/185] hackrtd.css --- docs/source/_static/hackrtd.css | 112 ++++++++++++++++++++++++++++++++ docs/source/conf.py | 8 +++ 2 files changed, 120 insertions(+) create mode 100644 docs/source/_static/hackrtd.css diff --git a/docs/source/_static/hackrtd.css b/docs/source/_static/hackrtd.css new file mode 100644 index 0000000..e75a889 --- /dev/null +++ b/docs/source/_static/hackrtd.css @@ -0,0 +1,112 @@ +/* Temporary hack to work around bug in rtd theme 2.0 through 2.4 + See https://github.com/rtfd/sphinx_rtd_theme/pull/382 +*/ +pre { + line-height: normal !important; +} + +/* Make .. deprecation:: blocks visible + * (by default they're entirely unstyled) + */ +.deprecated { + background-color: #ffe13b; +} + +/* Add a snakey triskelion ornament to
+ * https://stackoverflow.com/questions/8862344/css-hr-with-ornament/18541258#18541258 + * but only do it to
s in the content box, b/c the RTD popup control panel + * thingummy also has an
in it, and putting the ornament on that looks + * *really weird*. (In particular, the background color is wrong.) + */ +.rst-content hr:after { + /* This .svg gets displayed on top of the middle of the hrule. It has a box + * behind the logo that's colored to match the RTD theme body background + * color (#fcfcfc), which hides the middle part of the hrule to make it + * look like there's a gap in it. The size of the box determines the size + * of the gap. + */ + background: url('ornament.svg') no-repeat top center; + background-size: contain; + content: ""; + display: block; + height: 30px; + position: relative; + top: -15px; +} + +/* Hacks to make the upper-left logo area look nicer */ + +.wy-side-nav-search { + /* Lighter background color to match logo */ + background-color: #d2e7fa !important; +} + +.wy-side-nav-search > a { + color: #306998 !important; +} + +.wy-side-nav-search > a.logo { + display: block !important; + padding-bottom: 0.809em !important; +} + +.wy-side-nav-search > a img.logo { + display: inline !important; + padding: 0 !important; +} + +.trio-version { + display: inline; + /* I *cannot* figure out how to get the version text vertically centered + on the logo. Oh well... + height: 32px; + line-height: 32px; + */ +} + +.wy-side-nav-search > a { + /* Mostly this is just to simplify things, so we don't have margin/padding + * on both the and the inside it */ + margin: 0 !important; + padding: 0 !important; +} + +/* Get rid of the weird super dark "Contents" label that wastes vertical space + */ +.wy-menu-vertical > p.caption { + display: none !important; +} + +/* I do not like RTD's use of Roboto Slab for headlines. So force it back to + * Lato (or whatever fallback it's using if Lato isn't available for some + * reason). I also experimented with using Montserrat to be extra obnoxiously + * on brand, but honestly you couldn't really tell so there wasn't much point + * in adding page weight for that, and this is going to match the body text + * better. (Montserrat for body text *definitely* didn't look good, alas.) + */ +h1, h2, h3, h4, h5, h6, legend, .rst-content .toctree-wrapper p.caption { + font-family: inherit !important; +} + +/* Get rid of the horrible red for literal content */ +.rst-content tt.literal, .rst-content tt.literal, .rst-content code.literal { + color: #222 !important; +} + +/* Style the "Need help?" text just underneath the search box */ +.trio-help-hint { + line-height: normal; + margin-bottom: 0; + /* font-size: 12px; */ + font-size: 80%; /* matches the "Search docs" box */ + padding-top: 6px; + color: #306998; + text-align: center; +} + +a.trio-help-hint, .trio-help-hint a:link, .trio-help-hint a:visited { + color: inherit; + /* Like text-decoration: underline, but with a thinner line */ + text-decoration: none; + border-bottom: 1px solid; +} diff --git a/docs/source/conf.py b/docs/source/conf.py index d762180..70282d8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -42,6 +42,14 @@ ("py:obj", "bytes-like"), ] +# XX hack the RTD theme until +# https://github.com/rtfd/sphinx_rtd_theme/pull/382 +# is shipped (should be in the release after 0.2.4) +# ...note that this has since grown to contain a bunch of other CSS hacks too +# though. +def setup(app): + app.add_css_file("hackrtd.css") + # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. From 2d7781084e2a3e86cae7564b476d9bce6fbc933d Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 14 Oct 2020 11:14:38 -0400 Subject: [PATCH 147/185] tweak sidebartitle template --- docs/source/_templates/layout.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/source/_templates/layout.html b/docs/source/_templates/layout.html index f06fb9e..f8ec93e 100644 --- a/docs/source/_templates/layout.html +++ b/docs/source/_templates/layout.html @@ -4,8 +4,8 @@ {% extends "!layout.html" %} {% block sidebartitle %} - {{ project }} - + + {{ project }} {%- set nav_version = version %} {% if READTHEDOCS and current_version %} {%- set nav_version = current_version %} @@ -14,6 +14,8 @@ {% if nav_version != 'latest' %}
{{ nav_version }}
{% endif %} +
+
{% include "searchbox.html" %} From d5fb3bdeddcdcae86fa5a9cf416cc3b4bbe84332 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Wed, 14 Oct 2020 11:30:51 -0400 Subject: [PATCH 148/185] padding: 20 for logo --- docs/source/_static/hackrtd.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/_static/hackrtd.css b/docs/source/_static/hackrtd.css index e75a889..ea8f5a5 100644 --- a/docs/source/_static/hackrtd.css +++ b/docs/source/_static/hackrtd.css @@ -52,7 +52,7 @@ pre { .wy-side-nav-search > a img.logo { display: inline !important; - padding: 0 !important; + padding: 20 !important; } .trio-version { From e0db0ddc358aa0e52b5d07dbe1b1061fbd4eca46 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 15 Oct 2020 08:08:28 -0400 Subject: [PATCH 149/185] Copy ornament.svg over from Trio as well --- docs/source/_static/ornament.svg | 188 +++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 docs/source/_static/ornament.svg diff --git a/docs/source/_static/ornament.svg b/docs/source/_static/ornament.svg new file mode 100644 index 0000000..e4793e3 --- /dev/null +++ b/docs/source/_static/ornament.svg @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + From 51035fd839f873b470b33bac8bac6b7c1434b5b1 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Tue, 13 Oct 2020 20:24:35 -0400 Subject: [PATCH 150/185] Bump version and run towncrier for 0.7.0 release --- docs/source/history.rst | 15 +++++++++++++++ newsfragments/105.feature.rst | 1 - newsfragments/96.removal.rst | 1 - pytest_trio/_version.py | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) delete mode 100644 newsfragments/105.feature.rst delete mode 100644 newsfragments/96.removal.rst diff --git a/docs/source/history.rst b/docs/source/history.rst index c707e82..9ef63e5 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -5,6 +5,21 @@ Release history .. towncrier release notes start +pytest-trio 0.7.0 (2020-10-15) +------------------------------ + +Features +~~~~~~~~ + +- Support added for :ref:`alternative Trio run functions ` via the ``trio_run`` configuration variable and ``@pytest.mark.trio(run=...)``. Presently supports Trio and QTrio. (`#105 `__) + + +Deprecations and Removals +~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Python 3.5 support removed. (`#96 `__) + + pytest-trio 0.6.0 (2020-05-20) ---------------------------------- diff --git a/newsfragments/105.feature.rst b/newsfragments/105.feature.rst deleted file mode 100644 index 4b510a3..0000000 --- a/newsfragments/105.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Support added for :ref:`alternative Trio run functions ` via the ``trio_run`` configuration variable and ``@pytest.mark.trio(run=...)``. Presently supports Trio and QTrio. diff --git a/newsfragments/96.removal.rst b/newsfragments/96.removal.rst deleted file mode 100644 index 8051806..0000000 --- a/newsfragments/96.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Python 3.5 support removed. diff --git a/pytest_trio/_version.py b/pytest_trio/_version.py index dff3253..34fd540 100644 --- a/pytest_trio/_version.py +++ b/pytest_trio/_version.py @@ -1,3 +1,3 @@ # This file is imported from __init__.py and exec'd from setup.py -__version__ = "0.6.0+dev" +__version__ = "0.7.0" From fad87b38e3dd037474ac6636ae4889b50b14d1f7 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Thu, 15 Oct 2020 09:51:40 -0400 Subject: [PATCH 151/185] Bump version to 0.7.0+dev post release --- pytest_trio/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_trio/_version.py b/pytest_trio/_version.py index 34fd540..9d95beb 100644 --- a/pytest_trio/_version.py +++ b/pytest_trio/_version.py @@ -1,3 +1,3 @@ # This file is imported from __init__.py and exec'd from setup.py -__version__ = "0.7.0" +__version__ = "0.7.0+dev" From 21a8fb025c4a190239700190b6ce714ac37345d8 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Wed, 6 Jan 2021 10:01:37 +0400 Subject: [PATCH 152/185] Switch to GitHub Actions --- .appveyor.yml | 30 -------------- .github/workflows/ci.yml | 89 ++++++++++++++++++++++++++++++++++++++++ .travis.yml | 33 --------------- ci.sh | 81 ++++++++++++++++++++++++++++++++++++ ci/travis.sh | 63 ---------------------------- 5 files changed, 170 insertions(+), 126 deletions(-) delete mode 100644 .appveyor.yml create mode 100644 .github/workflows/ci.yml delete mode 100644 .travis.yml create mode 100755 ci.sh delete mode 100755 ci/travis.sh diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 29dd725..0000000 --- a/.appveyor.yml +++ /dev/null @@ -1,30 +0,0 @@ -skip_tags: true - -os: Visual Studio 2015 - -environment: - matrix: - - PYTHON: "C:\\Python36" - - PYTHON: "C:\\Python36-x64" - - PYTHON: "C:\\Python37" - - PYTHON: "C:\\Python37-x64" - - PYTHON: "C:\\Python38" - - PYTHON: "C:\\Python38-x64" - -build_script: - - "git --no-pager log -n2" - - "echo %APPVEYOR_REPO_COMMIT%" - - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;;%PATH%" - - "python --version" - - "python -c \"import struct; print(struct.calcsize('P') * 8)\"" - - "pip install ." - - "pip install -Ur test-requirements.txt" - - "pip install codecov" - -test_script: - - "mkdir empty" - - "cd empty" - # Make sure it's being imported from where we expect - - "python -c \"import os, pytest_trio; print(os.path.dirname(pytest_trio.__file__))\"" - - "python -u -m coverage run --rcfile=../.coveragerc -m pytest -W error -ra -v -s --pyargs pytest_trio" - - "codecov" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..45b317d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,89 @@ +name: CI + +on: + push: + branches-ignore: + - "dependabot/**" + pull_request: + +jobs: + Windows: + name: 'Windows (${{ matrix.python }})' + runs-on: 'windows-latest' + strategy: + fail-fast: false + matrix: + python: ['3.6', '3.7', '3.8', '3.9'] + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup python + uses: actions/setup-python@v2 + with: + python-version: '${{ matrix.python }}' + - name: Run tests + run: ./ci.sh + shell: bash + env: + # Should match 'name:' up above + JOB_NAME: 'Windows (${{ matrix.python }})' + + Ubuntu: + name: 'Ubuntu (${{ matrix.python }}${{ matrix.extra_name }})' + timeout-minutes: 10 + runs-on: 'ubuntu-latest' + strategy: + fail-fast: false + matrix: + python: ['pypy-3.6', 'pypy-3.7', '3.6', '3.7', '3.8', '3.9', '3.6-dev', '3.7-dev', '3.8-dev', '3.9-dev'] + check_formatting: ['0'] + check_docs: ['0'] + extra_name: [''] + include: + - python: '3.9' + check_formatting: '1' + extra_name: ', check formatting' + - python: '3.9' + check_docs: '1' + extra_name: ', check docs' + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup python + uses: actions/setup-python@v2 + if: "!endsWith(matrix.python, '-dev')" + with: + python-version: '${{ matrix.python }}' + - name: Setup python (dev) + uses: deadsnakes/action@v2.0.2 + if: endsWith(matrix.python, '-dev') + with: + python-version: '${{ matrix.python }}' + - name: Run tests + run: ./ci.sh + env: + CHECK_FORMATTING: '${{ matrix.check_formatting }}' + CHECK_DOCS: '${{ matrix.check_docs }}' + # Should match 'name:' up above + JOB_NAME: 'Ubuntu (${{ matrix.python }}${{ matrix.extra_name }})' + + macOS: + name: 'macOS (${{ matrix.python }})' + timeout-minutes: 10 + runs-on: 'macos-latest' + strategy: + fail-fast: false + matrix: + python: ['3.6', '3.7', '3.8', '3.9'] + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Setup python + uses: actions/setup-python@v2 + with: + python-version: '${{ matrix.python }}' + - name: Run tests + run: ./ci.sh + env: + # Should match 'name:' up above + JOB_NAME: 'macOS (${{ matrix.python }})' diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a52c6ce..0000000 --- a/.travis.yml +++ /dev/null @@ -1,33 +0,0 @@ -os: linux -dist: bionic -language: python -python: -- pypy3.6-7.2.0 -- 3.6 -- 3.6-dev -- 3.7 -- 3.7-dev -- 3.8 -- 3.8-dev -- 3.9-dev -jobs: - include: - - os: osx - language: generic - env: MACPYTHON=3.6.8 - - os: osx - language: generic - env: MACPYTHON=3.7.7 - - os: osx - language: generic - env: MACPYTHON=3.8.1 - - os: linux - language: python - python: 3.6 - env: CHECK_DOCS=1 - - os: linux - language: python - python: 3.6 - env: CHECK_FORMATTING=1 -script: -- ci/travis.sh diff --git a/ci.sh b/ci.sh new file mode 100755 index 0000000..ddcd21f --- /dev/null +++ b/ci.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +set -ex -o pipefail + +# Log some general info about the environment +uname -a +env | sort + +# See https://github.com/python-trio/trio/issues/334 +YAPF_VERSION=0.22.0 + +# Curl's built-in retry system is not very robust; it gives up on lots of +# network errors that we want to retry on. Wget might work better, but it's +# not installed on azure pipelines's windows boxes. So... let's try some good +# old-fashioned brute force. (This is also a convenient place to put options +# we always want, like -f to tell curl to give an error if the server sends an +# error response, and -L to follow redirects.) +function curl-harder() { + for BACKOFF in 0 1 2 4 8 15 15 15 15; do + sleep $BACKOFF + if curl -fL --connect-timeout 5 "$@"; then + return 0 + fi + done + return 1 +} + + +python -m pip install -U pip setuptools wheel +python -m pip --version + +python setup.py sdist --formats=zip +python -m pip install dist/*.zip + +if [ "$CHECK_FORMATTING" = "1" ]; then + pip install yapf==${YAPF_VERSION} + if ! yapf -rpd setup.py pytest_trio; then + cat < Date: Wed, 6 Jan 2021 10:06:43 +0400 Subject: [PATCH 153/185] Pin requirements We don't support pytest 6.2 yet, and want to switch to dependabot in the future. --- test-requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 5c53c25..c8280fc 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,3 @@ -pytest >= 6.0.0 # https://github.com/python-trio/pytest-trio/pull/98#issuecomment-678699693 -pytest-cov -hypothesis>=3.64 +pytest==6.1.2 +pytest-cov==2.10.1 +hypothesis==5.47.0 From 78c5362223cce91e6b6ab7a2442fcbbfd39ea67d Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Wed, 6 Jan 2021 10:21:25 +0400 Subject: [PATCH 154/185] Stop testing pypy3.7 which isn't supported by Trio yet `warn_on_full_buffer` support was added in PyPy, but it did not get released yet. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 45b317d..bf6e8a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['pypy-3.6', 'pypy-3.7', '3.6', '3.7', '3.8', '3.9', '3.6-dev', '3.7-dev', '3.8-dev', '3.9-dev'] + python: ['pypy-3.6', '3.6', '3.7', '3.8', '3.9', '3.6-dev', '3.7-dev', '3.8-dev', '3.9-dev'] check_formatting: ['0'] check_docs: ['0'] extra_name: [''] From 3d403f8d4bac3602d404fd025325189c5ba23b9a Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Mon, 11 Jan 2021 10:18:28 +0400 Subject: [PATCH 155/185] Fix CI badges --- README.rst | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index e919304..0d3b1a3 100644 --- a/README.rst +++ b/README.rst @@ -13,13 +13,9 @@ pytest-trio :target: https://pypi.org/project/pytest-trio :alt: Latest PyPi version -.. image:: https://travis-ci.org/python-trio/pytest-trio.svg?branch=master - :target: https://travis-ci.org/python-trio/pytest-trio - :alt: Automated test status (Linux and MacOS) - -.. image:: https://ci.appveyor.com/api/projects/status/aq0pklx7hanx031x?svg=true - :target: https://ci.appveyor.com/project/touilleMan/pytest-trio - :alt: Automated test status (Windows) +.. image:: https://github.com/python-trio/pytest-trio/workflows/CI/badge.svg?branch=master + :target: https://github.com/python-trio/pytest-trio/actions + :alt: Automated test status .. image:: https://codecov.io/gh/python-trio/pytest-trio/branch/master/graph/badge.svg :target: https://codecov.io/gh/python-trio/pytest-trio From d0c090689e11b7b68b23107fe1bfdf8cd3ec61fa Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Tue, 20 Apr 2021 20:56:35 +0200 Subject: [PATCH 156/185] Update docs-requirements.txt --- docs-requirements.txt | 143 ++++++++++++++++++++++++++++++------------ 1 file changed, 103 insertions(+), 40 deletions(-) diff --git a/docs-requirements.txt b/docs-requirements.txt index 5b3f91a..474c5ed 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -4,46 +4,109 @@ # # pip-compile --output-file=docs-requirements.txt docs-requirements.in # -alabaster==0.7.12 # via sphinx -async-generator==1.10 # via -r docs-requirements.in, trio -attrs==20.2.0 # via -r docs-requirements.in, outcome, pytest, trio -babel==2.8.0 # via sphinx -certifi==2020.6.20 # via requests -chardet==3.0.4 # via requests -click==7.1.2 # via towncrier -docutils==0.16 # via sphinx -idna==2.10 # via requests, trio -imagesize==1.2.0 # via sphinx -incremental==17.5.0 # via towncrier -iniconfig==1.0.1 # via pytest -jinja2==2.11.2 # via sphinx, towncrier -markupsafe==1.1.1 # via jinja2 -outcome==1.0.1 # via -r docs-requirements.in, trio -packaging==20.4 # via pytest, sphinx -pluggy==0.13.1 # via pytest -py==1.9.0 # via pytest -pygments==2.7.1 # via sphinx -pyparsing==2.4.7 # via packaging -pytest==6.1.1 # via -r docs-requirements.in -pytz==2020.1 # via babel -requests==2.24.0 # via sphinx -six==1.15.0 # via packaging -sniffio==1.2.0 # via trio -snowballstemmer==2.0.0 # via sphinx -sortedcontainers==2.2.2 # via trio -sphinx-rtd-theme==0.5.0 # via -r docs-requirements.in -sphinx==3.2.1 # via -r docs-requirements.in, sphinx-rtd-theme, sphinxcontrib-trio -sphinxcontrib-applehelp==1.0.2 # via sphinx -sphinxcontrib-devhelp==1.0.2 # via sphinx -sphinxcontrib-htmlhelp==1.0.3 # via sphinx -sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.3 # via sphinx -sphinxcontrib-serializinghtml==1.1.4 # via sphinx -sphinxcontrib-trio==1.1.2 # via -r docs-requirements.in -toml==0.10.1 # via pytest, towncrier -towncrier==19.2.0 # via -r docs-requirements.in -trio==0.17.0 # via -r docs-requirements.in -urllib3==1.25.10 # via requests +alabaster==0.7.12 + # via sphinx +async-generator==1.10 + # via + # -r docs-requirements.in + # trio +attrs==20.3.0 + # via + # -r docs-requirements.in + # outcome + # pytest + # trio +babel==2.9.0 + # via sphinx +certifi==2020.12.5 + # via requests +chardet==4.0.0 + # via requests +click-default-group==1.2.2 + # via towncrier +click==7.1.2 + # via + # click-default-group + # towncrier +docutils==0.16 + # via + # sphinx + # sphinx-rtd-theme +idna==2.10 + # via + # requests + # trio +imagesize==1.2.0 + # via sphinx +incremental==21.3.0 + # via towncrier +iniconfig==1.1.1 + # via pytest +jinja2==2.11.3 + # via + # sphinx + # towncrier +markupsafe==1.1.1 + # via jinja2 +outcome==1.1.0 + # via + # -r docs-requirements.in + # trio +packaging==20.9 + # via + # pytest + # sphinx +pluggy==0.13.1 + # via pytest +py==1.10.0 + # via pytest +pygments==2.8.1 + # via sphinx +pyparsing==2.4.7 + # via packaging +pytest==6.2.3 + # via -r docs-requirements.in +pytz==2021.1 + # via babel +requests==2.25.1 + # via sphinx +sniffio==1.2.0 + # via trio +snowballstemmer==2.1.0 + # via sphinx +sortedcontainers==2.3.0 + # via trio +sphinx-rtd-theme==0.5.2 + # via -r docs-requirements.in +sphinx==3.5.4 + # via + # -r docs-requirements.in + # sphinx-rtd-theme + # sphinxcontrib-trio +sphinxcontrib-applehelp==1.0.2 + # via sphinx +sphinxcontrib-devhelp==1.0.2 + # via sphinx +sphinxcontrib-htmlhelp==1.0.3 + # via sphinx +sphinxcontrib-jsmath==1.0.1 + # via sphinx +sphinxcontrib-qthelp==1.0.3 + # via sphinx +sphinxcontrib-serializinghtml==1.1.4 + # via sphinx +sphinxcontrib-trio==1.1.2 + # via -r docs-requirements.in +toml==0.10.2 + # via + # pytest + # towncrier +towncrier==21.3.0 + # via -r docs-requirements.in +trio==0.18.0 + # via -r docs-requirements.in +urllib3==1.26.4 + # via requests # The following packages are considered to be unsafe in a requirements file: # setuptools From 5669bc0e9f8cf472c2bf7b418c59d7ace631de4a Mon Sep 17 00:00:00 2001 From: Emmanuel Leblond Date: Tue, 20 Apr 2021 21:18:53 +0200 Subject: [PATCH 157/185] Remove Cpython 3.6-dev and 3.7-dev from CI (no longer available) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf6e8a5..9a68a8c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['pypy-3.6', '3.6', '3.7', '3.8', '3.9', '3.6-dev', '3.7-dev', '3.8-dev', '3.9-dev'] + python: ['pypy-3.6', '3.6', '3.7', '3.8', '3.9', '3.8-dev', '3.9-dev'] check_formatting: ['0'] check_docs: ['0'] extra_name: [''] From e0509f9f5612be55ac954a4b1acb1a8c7417ca85 Mon Sep 17 00:00:00 2001 From: Dan Drabinski Date: Fri, 30 Apr 2021 16:56:57 -0400 Subject: [PATCH 158/185] Fix comment typos in test_async_fixture --- pytest_trio/_tests/test_async_fixture.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pytest_trio/_tests/test_async_fixture.py b/pytest_trio/_tests/test_async_fixture.py index 8fb5875..9f1d4d4 100644 --- a/pytest_trio/_tests/test_async_fixture.py +++ b/pytest_trio/_tests/test_async_fixture.py @@ -116,9 +116,9 @@ async def test_simple(sync_fix): # In pytest, ERROR status occurs when an exception is raised in fixture code. -# The trouble is our async fixtures must be run whithin a trio context, hence +# The trouble is our async fixtures must be run within a trio context, hence # they are actually run just before the test, providing no way to make the -# difference between an exception comming from the real test or from an +# difference between an exception coming from the real test or from an # async fixture... @pytest.mark.xfail(reason='Not implemented yet') def test_raise_in_async_fixture_cause_pytest_error(testdir): @@ -133,7 +133,7 @@ async def fix1(): @pytest.mark.trio async def test_base(fix1): - pass # Crash should have occures before arriving here + pass # Crash should have occurs before arriving here """ ) From 2a8fdaf388996ce9bc5a4746d30e5a5a8bbb0fd6 Mon Sep 17 00:00:00 2001 From: Dan Drabinski <35439959+ddrabin@users.noreply.github.com> Date: Fri, 30 Apr 2021 17:12:51 -0400 Subject: [PATCH 159/185] Update pytest_trio/_tests/test_async_fixture.py Co-authored-by: Nathaniel J. Smith --- pytest_trio/_tests/test_async_fixture.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_trio/_tests/test_async_fixture.py b/pytest_trio/_tests/test_async_fixture.py index 9f1d4d4..9cbd0b7 100644 --- a/pytest_trio/_tests/test_async_fixture.py +++ b/pytest_trio/_tests/test_async_fixture.py @@ -133,7 +133,7 @@ async def fix1(): @pytest.mark.trio async def test_base(fix1): - pass # Crash should have occurs before arriving here + pass # Crash should have occurred before arriving here """ ) From 7e6703472293f640b2386ffa221e00cd8a686177 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Wed, 5 May 2021 22:04:13 -0700 Subject: [PATCH 160/185] When fixture setup crashes, immediately cancel all other fixture setups Fixes gh-120 --- newsfragments/120.bugfix.rst | 3 ++ pytest_trio/_tests/test_fixture_ordering.py | 46 +++++++++++++++++++- pytest_trio/plugin.py | 48 ++++++++++++++------- 3 files changed, 79 insertions(+), 18 deletions(-) create mode 100644 newsfragments/120.bugfix.rst diff --git a/newsfragments/120.bugfix.rst b/newsfragments/120.bugfix.rst new file mode 100644 index 0000000..cd60b41 --- /dev/null +++ b/newsfragments/120.bugfix.rst @@ -0,0 +1,3 @@ +Fix an issue where if two fixtures are being set up concurrently, and +one crashes and the other hangs, then the test as a whole would hang, +rather than being cancelled and unwound after the crash. diff --git a/pytest_trio/_tests/test_fixture_ordering.py b/pytest_trio/_tests/test_fixture_ordering.py index eb73ca9..8af1cdb 100644 --- a/pytest_trio/_tests/test_fixture_ordering.py +++ b/pytest_trio/_tests/test_fixture_ordering.py @@ -154,13 +154,15 @@ def test_error_collection(testdir): @trio_fixture async def crash_nongen(): - await trio.sleep(2) + with trio.CancelScope(shield=True): + await trio.sleep(2) raise RuntimeError("crash_nongen".upper()) @trio_fixture @async_generator async def crash_early_agen(): - await trio.sleep(2) + with trio.CancelScope(shield=True): + await trio.sleep(2) raise RuntimeError("crash_early_agen".upper()) await yield_() @@ -330,3 +332,43 @@ async def test_try(fixture): result.stdout.fnmatch_lines_random([ "*OOPS*", ]) + + +# Makes sure that +# See https://github.com/python-trio/pytest-trio/issues/120 +def test_fixtures_crash_and_hang_concurrently(testdir): + testdir.makepyfile( + """ + import trio + import pytest + + + @pytest.fixture + async def hanging_fixture(): + print("hanging_fixture:start") + await trio.Event().wait() + yield + print("hanging_fixture:end") + + + @pytest.fixture + async def exploding_fixture(): + print("exploding_fixture:start") + raise Exception + yield + print("exploding_fixture:end") + + + @pytest.mark.trio + async def test_fails_right_away(exploding_fixture): + ... + + + @pytest.mark.trio + async def test_fails_needs_some_scopes(exploding_fixture, hanging_fixture): + ... + """ + ) + + result = testdir.runpytest() + result.assert_outcomes(passed=0, failed=2) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 61c4181..47a8eed 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -121,11 +121,12 @@ def pytest_exception_interact(node, call, report): # If a fixture crashes, whether during setup, teardown, or in a background # task at any other point, then we mark the whole test run as "crashed". When # a run is "crashed", two things happen: (1) if any fixtures or the test -# itself haven't started yet, then we don't start them. (2) if the test is -# running, we cancel it. That's all. In particular, if a fixture has a -# background crash, we don't propagate that to any other fixtures, we still -# follow the normal teardown sequence, and so on – but since the test is -# cancelled, the teardown sequence should start immediately. +# itself haven't started yet, then we don't start them, and treat them as if +# they've already exited. (2) if the test is running, we cancel it. That's +# all. In particular, if a fixture has a background crash, we don't propagate +# that to any other fixtures, we still follow the normal teardown sequence, +# and so on – but since the test is cancelled, the teardown sequence should +# start immediately. canary = contextvars.ContextVar("pytest-trio canary") @@ -133,7 +134,12 @@ def pytest_exception_interact(node, call, report): class TrioTestContext: def __init__(self): self.crashed = False - self.test_cancel_scope = None + # This holds cancel scopes for whatever setup steps are currently + # running -- initially it's the fixtures that are in the middle of + # evaluating themselves, and then once fixtures are set up it's the + # test itself. Basically, at any given moment, it's the stuff we need + # to cancel if we want to start tearing down our fixture DAG. + self.active_cancel_scopes = set() self.fixtures_with_errors = set() self.fixtures_with_cancel = set() self.error_list = [] @@ -145,8 +151,8 @@ def crash(self, fixture, exc): self.error_list.append(exc) self.fixtures_with_errors.add(fixture) self.crashed = True - if self.test_cancel_scope is not None: - self.test_cancel_scope.cancel() + for cscope in self.active_cancel_scopes: + cscope.cancel() class TrioFixture: @@ -240,16 +246,17 @@ async def run(self, test_ctx, contextvars_ctx): return # Run actual fixture setup step + # If another fixture crashes while we're in the middle of setting + # up, we want to be cancelled immediately, so we'll save an + # encompassing cancel scope where self._crash can find it. + test_ctx.active_cancel_scopes.add(nursery_fixture.cancel_scope) if self._is_test: - # Tests are exactly like fixtures, except that they (1) have - # to be regular async functions, (2) if there's a crash, we - # should cancel them. + # Tests are exactly like fixtures, except that they to be + # regular async functions. assert not self.user_done_events func_value = None - with trio.CancelScope() as cancel_scope: - test_ctx.test_cancel_scope = cancel_scope - assert not test_ctx.crashed - await self._func(**resolved_kwargs) + assert not test_ctx.crashed + await self._func(**resolved_kwargs) else: func_value = self._func(**resolved_kwargs) if isinstance(func_value, Coroutine): @@ -261,8 +268,17 @@ async def run(self, test_ctx, contextvars_ctx): else: # Regular synchronous function self.fixture_value = func_value + # Now that we're done setting up, we don't want crashes to cancel + # us immediately; instead we want them to cancel our downstream + # dependents, and then eventually let us clean up normally. So + # remove this from the set of cancel scopes affected by self._crash. + test_ctx.active_cancel_scopes.remove(nursery_fixture.cancel_scope) - # Notify our users that self.fixture_value is ready + + # self.fixture_value is ready, so notify users that they can + # continue. (Or, maybe we crashed and were cancelled, in which + # case our users will check test_ctx.crashed and immediately exit, + # which is fine too.) self.setup_done.set() # Wait for users to be finished From d7a98c5cdc0fec2306c8bc2fad7c11f989c46b8a Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Thu, 6 May 2021 23:54:17 -0700 Subject: [PATCH 161/185] Run yapf --- pytest_trio/plugin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 47a8eed..8ddb107 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -274,7 +274,6 @@ async def run(self, test_ctx, contextvars_ctx): # remove this from the set of cancel scopes affected by self._crash. test_ctx.active_cancel_scopes.remove(nursery_fixture.cancel_scope) - # self.fixture_value is ready, so notify users that they can # continue. (Or, maybe we crashed and were cancelled, in which # case our users will check test_ctx.crashed and immediately exit, From b338795587c90130f1461568bb3d4426b3947512 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Fri, 7 May 2021 00:39:19 -0700 Subject: [PATCH 162/185] Pin older version of towncrier Workaround for https://github.com/twisted/towncrier/issues/346 --- docs-requirements.in | 5 +++-- docs-requirements.txt | 10 +++------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/docs-requirements.in b/docs-requirements.in index f86bf78..4b299a1 100644 --- a/docs-requirements.in +++ b/docs-requirements.in @@ -5,8 +5,9 @@ sphinxcontrib-trio # Workaround for this weird issue: # https://travis-ci.org/python-trio/pytest-trio/jobs/407495415 attrs >= 17.4.0 -# != 19.9.0rc1 for https://github.com/twisted/towncrier/issues/180 -towncrier != 19.9.0rc1 +# != 19.9.0 for https://github.com/twisted/towncrier/issues/180 +# != 21.3.0 for https://github.com/twisted/towncrier/issues/346 +towncrier != 19.9.0,!= 21.3.0 # pytest-trio's own dependencies trio >= 0.15.0 diff --git a/docs-requirements.txt b/docs-requirements.txt index 474c5ed..b401bbf 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --output-file=docs-requirements.txt docs-requirements.in +# pip-compile docs-requirements.in # alabaster==0.7.12 # via sphinx @@ -22,12 +22,8 @@ certifi==2020.12.5 # via requests chardet==4.0.0 # via requests -click-default-group==1.2.2 - # via towncrier click==7.1.2 - # via - # click-default-group - # towncrier + # via towncrier docutils==0.16 # via # sphinx @@ -101,7 +97,7 @@ toml==0.10.2 # via # pytest # towncrier -towncrier==21.3.0 +towncrier==19.2.0 # via -r docs-requirements.in trio==0.18.0 # via -r docs-requirements.in From c2c92917b632fa40347cf63efcdcf1f6b5ae3c31 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Tue, 7 Jun 2022 09:47:31 +0400 Subject: [PATCH 163/185] Make setuptools license field SPDX-compliant https://spdx.org/licenses/ https://spdx.github.io/spdx-spec/SPDX-license-expressions/#d42-disjunctive-or-operator --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 054ffca..58a2572 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ long_description=open("README.rst").read(), author="Emmanuel Leblond", author_email="emmanuel.leblond@gmail.com", - license="MIT -or- Apache License 2.0", + license="MIT OR Apache-2.0", packages=find_packages(), entry_points={'pytest11': ['trio = pytest_trio.plugin']}, install_requires=[ From 8bf4d5d0654eb524635413443ba21523aeb23f36 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sun, 30 Oct 2022 01:11:23 -0700 Subject: [PATCH 164/185] Use ASCII dashes --- docs/source/quickstart.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 0ef3164..490cc12 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -183,7 +183,7 @@ If you need to support Python 3.5, which doesn't allow ``yield`` inside an ``async def`` function, then you can define async fixtures using the `async_generator `__ -library – just make sure to put the ``@pytest.fixture`` *above* the +library - just make sure to put the ``@pytest.fixture`` *above* the ``@async_generator``. @@ -232,7 +232,7 @@ Here's a first attempt:: This will mostly work, but it has a few problems. The most obvious one is that when we run it, even if everything works perfectly, it will -hang at the end of the test – we never shut down the server, so the +hang at the end of the test - we never shut down the server, so the nursery block will wait forever for it to exit. To avoid this, we should cancel the nursery at the end of the test: @@ -292,9 +292,9 @@ you afterwards: Next problem: we have a race condition. We spawn a background task to call ``serve_tcp``, and then immediately try to connect to that server. Sometimes this will work fine. But it takes a little while for -the server to start up and be ready to accept connections – so other +the server to start up and be ready to accept connections - so other times, randomly, our connection attempt will happen too quickly, and -error out. After all – ``nursery.start_soon`` only promises that the +error out. After all - ``nursery.start_soon`` only promises that the task will be started *soon*, not that it has actually happened. So this test will be flaky, and flaky tests are the worst. @@ -378,7 +378,7 @@ Putting it all together: await echo_client.send_all(test_byte) assert await echo_client.receive_some(1) == test_byte -Now, this works – but there's still a lot of boilerplate. Remember, we +Now, this works - but there's still a lot of boilerplate. Remember, we need to write lots of tests for this server, and we don't want to have to copy-paste all that stuff into every test. Let's factor out the setup into a fixture:: From a071cfeb828e3c96cd5312a2db8617411abe5b78 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sun, 30 Oct 2022 01:11:23 -0700 Subject: [PATCH 165/185] Test on newer Python versions --- .github/workflows/ci.yml | 6 +++--- setup.py | 7 ++++++- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a68a8c..e9544ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['3.6', '3.7', '3.8', '3.9'] + python: ['3.7', '3.8', '3.9', '3.10', '3.11'] steps: - name: Checkout uses: actions/checkout@v2 @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['pypy-3.6', '3.6', '3.7', '3.8', '3.9', '3.8-dev', '3.9-dev'] + python: ['pypy-3.7', '3.7', 'pypy-3.8', '3.8', 'pypy-3.9', '3.9', '3.10', '3.11'] check_formatting: ['0'] check_docs: ['0'] extra_name: [''] @@ -74,7 +74,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['3.6', '3.7', '3.8', '3.9'] + python: ['3.7', '3.8', '3.9', '3.10', '3.11'] steps: - name: Checkout uses: actions/checkout@v2 diff --git a/setup.py b/setup.py index 054ffca..1666d5f 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ 'testing', 'trio', ], - python_requires=">=3.6", + python_requires=">=3.7", classifiers=[ "License :: OSI Approved :: MIT License", "License :: OSI Approved :: Apache Software License", @@ -36,6 +36,11 @@ "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: System :: Networking", From 222f6ff056edd4c746bf4529a4153067aa807d84 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sun, 30 Oct 2022 01:11:23 -0700 Subject: [PATCH 166/185] Update dependencies --- docs-requirements.in | 8 ++--- docs-requirements.txt | 78 ++++++++++++++++++++++++------------------- docs/source/conf.py | 2 +- setup.py | 7 ++-- test-requirements.txt | 6 ++-- 5 files changed, 54 insertions(+), 47 deletions(-) diff --git a/docs-requirements.in b/docs-requirements.in index 4b299a1..4e295c0 100644 --- a/docs-requirements.in +++ b/docs-requirements.in @@ -1,4 +1,3 @@ -# RTD is currently installing 1.5.3, which has a bug in :lineno-match: sphinx >= 1.6.1 sphinx_rtd_theme sphinxcontrib-trio @@ -10,8 +9,7 @@ attrs >= 17.4.0 towncrier != 19.9.0,!= 21.3.0 # pytest-trio's own dependencies -trio >= 0.15.0 +trio >= 0.22.0 async_generator >= 1.9 -outcome -# For node.get_closest_marker -pytest >= 3.6 +outcome >= 1.1.0 +pytest >= 7.2.0 diff --git a/docs-requirements.txt b/docs-requirements.txt index b401bbf..b81aba4 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile +# This file is autogenerated by pip-compile with python 3.8 # To update, run: # # pip-compile docs-requirements.in @@ -10,99 +10,109 @@ async-generator==1.10 # via # -r docs-requirements.in # trio -attrs==20.3.0 +attrs==22.1.0 # via # -r docs-requirements.in # outcome # pytest # trio -babel==2.9.0 +babel==2.10.3 # via sphinx -certifi==2020.12.5 +certifi==2022.9.24 # via requests -chardet==4.0.0 +charset-normalizer==2.1.1 # via requests -click==7.1.2 +click==8.1.3 + # via + # click-default-group + # towncrier +click-default-group==1.2.2 # via towncrier -docutils==0.16 +docutils==0.17.1 # via # sphinx # sphinx-rtd-theme -idna==2.10 +exceptiongroup==1.0.0 + # via + # pytest + # trio +idna==3.4 # via # requests # trio -imagesize==1.2.0 +imagesize==1.4.1 # via sphinx -incremental==21.3.0 +importlib-metadata==5.0.0 + # via sphinx +incremental==22.10.0 # via towncrier iniconfig==1.1.1 # via pytest -jinja2==2.11.3 +jinja2==3.1.2 # via # sphinx # towncrier -markupsafe==1.1.1 +markupsafe==2.1.1 # via jinja2 -outcome==1.1.0 +outcome==1.2.0 # via # -r docs-requirements.in # trio -packaging==20.9 +packaging==21.3 # via # pytest # sphinx -pluggy==0.13.1 - # via pytest -py==1.10.0 +pluggy==1.0.0 # via pytest -pygments==2.8.1 +pygments==2.13.0 # via sphinx -pyparsing==2.4.7 +pyparsing==3.0.9 # via packaging -pytest==6.2.3 +pytest==7.2.0 # via -r docs-requirements.in -pytz==2021.1 +pytz==2022.5 # via babel -requests==2.25.1 +requests==2.28.1 # via sphinx -sniffio==1.2.0 +sniffio==1.3.0 # via trio -snowballstemmer==2.1.0 +snowballstemmer==2.2.0 # via sphinx -sortedcontainers==2.3.0 +sortedcontainers==2.4.0 # via trio -sphinx-rtd-theme==0.5.2 - # via -r docs-requirements.in -sphinx==3.5.4 +sphinx==5.3.0 # via # -r docs-requirements.in # sphinx-rtd-theme # sphinxcontrib-trio +sphinx-rtd-theme==1.0.0 + # via -r docs-requirements.in sphinxcontrib-applehelp==1.0.2 # via sphinx sphinxcontrib-devhelp==1.0.2 # via sphinx -sphinxcontrib-htmlhelp==1.0.3 +sphinxcontrib-htmlhelp==2.0.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx sphinxcontrib-qthelp==1.0.3 # via sphinx -sphinxcontrib-serializinghtml==1.1.4 +sphinxcontrib-serializinghtml==1.1.5 # via sphinx sphinxcontrib-trio==1.1.2 # via -r docs-requirements.in -toml==0.10.2 +tomli==2.0.1 # via # pytest # towncrier -towncrier==19.2.0 +towncrier==22.8.0 # via -r docs-requirements.in -trio==0.18.0 +trio==0.22.0 # via -r docs-requirements.in -urllib3==1.26.4 +urllib3==1.26.12 # via requests +zipp==3.10.0 + # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/docs/source/conf.py b/docs/source/conf.py index 70282d8..4295707 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -109,7 +109,7 @@ def setup(app): # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. diff --git a/setup.py b/setup.py index 1666d5f..b79a918 100644 --- a/setup.py +++ b/setup.py @@ -16,11 +16,10 @@ packages=find_packages(), entry_points={'pytest11': ['trio = pytest_trio.plugin']}, install_requires=[ - "trio >= 0.15.0", + "trio >= 0.22.0", # for ExceptionGroup support "async_generator >= 1.9", - "outcome", - # For node.get_closest_marker - "pytest >= 3.6" + "outcome >= 1.1.0", + "pytest >= 7.2.0", # for ExceptionGroup support ], keywords=[ 'async', diff --git a/test-requirements.txt b/test-requirements.txt index c8280fc..b1d1509 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,3 @@ -pytest==6.1.2 -pytest-cov==2.10.1 -hypothesis==5.47.0 +pytest==7.2.0 +pytest-cov==4.0.0 +hypothesis==6.56.4 From f338c591fff6ddf75b288dd17cd6d42e6851f620 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sun, 30 Oct 2022 01:11:24 -0700 Subject: [PATCH 167/185] Improve too-many-Clocks message --- pytest_trio/plugin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 8ddb107..986ee52 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -343,13 +343,13 @@ def decorator(fn): @wraps(fn) def wrapper(**kwargs): __tracebackhide__ = True - clocks = [c for c in kwargs.values() if isinstance(c, Clock)] + clocks = {k: c for k, c in kwargs.items() if isinstance(c, Clock)} if not clocks: clock = None elif len(clocks) == 1: - clock = clocks[0] + clock = list(clocks.values())[0] else: - raise ValueError("too many clocks spoil the broth!") + raise ValueError(f"Expected at most one Clock in kwargs, got {clocks!r}") instruments = [ i for i in kwargs.values() if isinstance(i, Instrument) ] From 66188cb0ae548db7b9df2d4dfc92d441a1def1c1 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sun, 30 Oct 2022 01:11:24 -0700 Subject: [PATCH 168/185] Switch from YAPF to Black --- .style.yapf | 180 ------------------ CHEATSHEET.rst | 10 +- MANIFEST.in | 2 +- ci.sh | 11 +- docs/source/conf.py | 69 +++---- pytest_trio/_tests/helpers.py | 9 +- pytest_trio/_tests/test_async_fixture.py | 2 +- .../_tests/test_async_yield_fixture.py | 24 +-- pytest_trio/_tests/test_basic.py | 2 +- pytest_trio/_tests/test_fixture_mistakes.py | 12 +- pytest_trio/_tests/test_fixture_nursery.py | 4 +- pytest_trio/_tests/test_fixture_ordering.py | 8 +- .../_tests/test_hypothesis_interaction.py | 7 +- pytest_trio/_tests/test_sync_fixture.py | 8 +- pytest_trio/_tests/test_trio_mode.py | 12 +- pytest_trio/plugin.py | 65 +++---- setup.py | 10 +- 17 files changed, 110 insertions(+), 325 deletions(-) delete mode 100644 .style.yapf diff --git a/.style.yapf b/.style.yapf deleted file mode 100644 index 2c71c9d..0000000 --- a/.style.yapf +++ /dev/null @@ -1,180 +0,0 @@ -[style] -# Align closing bracket with visual indentation. -align_closing_bracket_with_visual_indent=True - -# Allow dictionary keys to exist on multiple lines. For example: -# -# x = { -# ('this is the first element of a tuple', -# 'this is the second element of a tuple'): -# value, -# } -allow_multiline_dictionary_keys=False - -# Allow lambdas to be formatted on more than one line. -allow_multiline_lambdas=False - -# Insert a blank line before a class-level docstring. -blank_line_before_class_docstring=False - -# Insert a blank line before a 'def' or 'class' immediately nested -# within another 'def' or 'class'. For example: -# -# class Foo: -# # <------ this blank line -# def method(): -# ... -blank_line_before_nested_class_or_def=False - -# Do not split consecutive brackets. Only relevant when -# dedent_closing_brackets is set. For example: -# -# call_func_that_takes_a_dict( -# { -# 'key1': 'value1', -# 'key2': 'value2', -# } -# ) -# -# would reformat to: -# -# call_func_that_takes_a_dict({ -# 'key1': 'value1', -# 'key2': 'value2', -# }) -coalesce_brackets=False - -# The column limit. -column_limit=79 - -# Indent width used for line continuations. -continuation_indent_width=4 - -# Put closing brackets on a separate line, dedented, if the bracketed -# expression can't fit in a single line. Applies to all kinds of brackets, -# including function definitions and calls. For example: -# -# config = { -# 'key1': 'value1', -# 'key2': 'value2', -# } # <--- this bracket is dedented and on a separate line -# -# time_series = self.remote_client.query_entity_counters( -# entity='dev3246.region1', -# key='dns.query_latency_tcp', -# transform=Transformation.AVERAGE(window=timedelta(seconds=60)), -# start_ts=now()-timedelta(days=3), -# end_ts=now(), -# ) # <--- this bracket is dedented and on a separate line -dedent_closing_brackets=True - -# Place each dictionary entry onto its own line. -each_dict_entry_on_separate_line=True - -# The regex for an i18n comment. The presence of this comment stops -# reformatting of that line, because the comments are required to be -# next to the string they translate. -i18n_comment= - -# The i18n function call names. The presence of this function stops -# reformattting on that line, because the string it has cannot be moved -# away from the i18n comment. -i18n_function_call= - -# Indent the dictionary value if it cannot fit on the same line as the -# dictionary key. For example: -# -# config = { -# 'key1': -# 'value1', -# 'key2': value1 + -# value2, -# } -indent_dictionary_value=True - -# The number of columns to use for indentation. -indent_width=4 - -# Join short lines into one line. E.g., single line 'if' statements. -join_multiple_lines=False - -# Use spaces around default or named assigns. -spaces_around_default_or_named_assign=False - -# Use spaces around the power operator. -spaces_around_power_operator=False - -# The number of spaces required before a trailing comment. -spaces_before_comment=2 - -# Insert a space between the ending comma and closing bracket of a list, -# etc. -space_between_ending_comma_and_closing_bracket=False - -# Split before arguments if the argument list is terminated by a -# comma. -split_arguments_when_comma_terminated=True - -# Set to True to prefer splitting before '&', '|' or '^' rather than -# after. -split_before_bitwise_operator=True - -# Split before a dictionary or set generator (comp_for). For example, note -# the split before the 'for': -# -# foo = { -# variable: 'Hello world, have a nice day!' -# for variable in bar if variable != 42 -# } -split_before_dict_set_generator=True - -# If an argument / parameter list is going to be split, then split before -# the first argument. -split_before_first_argument=True - -# Set to True to prefer splitting before 'and' or 'or' rather than -# after. -split_before_logical_operator=True - -# Split named assignments onto individual lines. -split_before_named_assigns=True - -# The penalty for splitting right after the opening bracket. -split_penalty_after_opening_bracket=30 - -# The penalty for splitting the line after a unary operator. -split_penalty_after_unary_operator=10000 - -# The penalty for splitting right before an if expression. -split_penalty_before_if_expr=0 - -# The penalty of splitting the line around the '&', '|', and '^' -# operators. -split_penalty_bitwise_operator=300 - -# The penalty for characters over the column limit. -split_penalty_excess_character=4500 - -# The penalty incurred by adding a line split to the unwrapped line. The -# more line splits added the higher the penalty. -split_penalty_for_added_line_split=30 - -# The penalty of splitting a list of "import as" names. For example: -# -# from a_very_long_or_indented_module_name_yada_yad import (long_argument_1, -# long_argument_2, -# long_argument_3) -# -# would reformat to something like: -# -# from a_very_long_or_indented_module_name_yada_yad import ( -# long_argument_1, long_argument_2, long_argument_3) -split_penalty_import_names=0 - -# The penalty of splitting the line around the 'and' and 'or' -# operators. -split_penalty_logical_operator=0 - -# Use the Tab character for indentation. -use_tabs=False - diff --git a/CHEATSHEET.rst b/CHEATSHEET.rst index 49a8b8d..aeacf0a 100644 --- a/CHEATSHEET.rst +++ b/CHEATSHEET.rst @@ -9,15 +9,7 @@ To run tests * Actually run the tests: ``pytest pytest_trio`` - -To run yapf ------------ - -* Show what changes yapf wants to make: ``yapf -rpd setup.py - pytest_trio`` - -* Apply all changes directly to the source tree: ``yapf -rpi setup.py - pytest_trio`` +* Format the code with ``black .`` To make a release diff --git a/MANIFEST.in b/MANIFEST.in index 4edd4b1..afc90e2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,5 @@ include README.rst CHEATSHEET.rst LICENSE* CODE_OF_CONDUCT* CONTRIBUTING* -include .coveragerc .style.yapf +include .coveragerc include test-requirements.txt recursive-include docs * prune docs/build diff --git a/ci.sh b/ci.sh index ddcd21f..aae8b3c 100755 --- a/ci.sh +++ b/ci.sh @@ -6,9 +6,6 @@ set -ex -o pipefail uname -a env | sort -# See https://github.com/python-trio/trio/issues/334 -YAPF_VERSION=0.22.0 - # Curl's built-in retry system is not very robust; it gives up on lots of # network errors that we want to retry on. Wget might work better, but it's # not installed on azure pipelines's windows boxes. So... let's try some good @@ -33,16 +30,16 @@ python setup.py sdist --formats=zip python -m pip install dist/*.zip if [ "$CHECK_FORMATTING" = "1" ]; then - pip install yapf==${YAPF_VERSION} - if ! yapf -rpd setup.py pytest_trio; then + pip install black + if ! black --check . ; then cat <=36', 'async_generator']) +@pytest.fixture(params=["Python>=36", "async_generator"]) def async_yield_implementation(request): - if request.param == 'Python>=36': + if request.param == "Python>=36": def patch_code(code): # Convert code to use Python>=3.6 builtin async generator - code = re.sub(r'(?m)^\s*@async_generator\n', r'', code) - code = re.sub(r'await yield_', r'yield', code) + code = re.sub(r"(?m)^\s*@async_generator\n", r"", code) + code = re.sub(r"await yield_", r"yield", code) return code return patch_code @@ -121,9 +121,7 @@ def test_after(): result.assert_outcomes(passed=3) -def test_async_yield_fixture_within_sync_fixture( - testdir, async_yield_implementation -): +def test_async_yield_fixture_within_sync_fixture(testdir, async_yield_implementation): testdir.makepyfile( async_yield_implementation( @@ -174,7 +172,7 @@ def test_after(): def test_async_yield_fixture_within_sync_yield_fixture( - testdir, async_yield_implementation + testdir, async_yield_implementation ): testdir.makepyfile( @@ -230,9 +228,7 @@ def test_after(): result.assert_outcomes(passed=3) -def test_async_yield_fixture_with_multiple_yields( - testdir, async_yield_implementation -): +def test_async_yield_fixture_with_multiple_yields(testdir, async_yield_implementation): testdir.makepyfile( async_yield_implementation( @@ -304,7 +300,7 @@ async def test_actual_test(server): def test_async_yield_fixture_crashed_teardown_allow_other_teardowns( - testdir, async_yield_implementation + testdir, async_yield_implementation ): testdir.makepyfile( @@ -358,6 +354,4 @@ def test_after(): result = testdir.runpytest() result.assert_outcomes(failed=1, passed=2) - result.stdout.re_match_lines( - [r'E\W+RuntimeError: Crash during fixture teardown'] - ) + result.stdout.re_match_lines([r"E\W+RuntimeError: Crash during fixture teardown"]) diff --git a/pytest_trio/_tests/test_basic.py b/pytest_trio/_tests/test_basic.py index c687d53..65dd911 100644 --- a/pytest_trio/_tests/test_basic.py +++ b/pytest_trio/_tests/test_basic.py @@ -58,7 +58,7 @@ def test_check_async_test_called(): result.assert_outcomes(passed=2) -@pytest.mark.xfail(reason='Raises pytest internal error so far...') +@pytest.mark.xfail(reason="Raises pytest internal error so far...") def test_sync_function_with_trio_mark(testdir): testdir.makepyfile( diff --git a/pytest_trio/_tests/test_fixture_mistakes.py b/pytest_trio/_tests/test_fixture_mistakes.py index 077861d..b00de9f 100644 --- a/pytest_trio/_tests/test_fixture_mistakes.py +++ b/pytest_trio/_tests/test_fixture_mistakes.py @@ -35,9 +35,7 @@ def test_sync_indirect(indirect_trio_time): result = testdir.runpytest() result.assert_outcomes(passed=1, errors=2) - result.stdout.fnmatch_lines( - ["*: Trio fixtures can only be used by Trio tests*"] - ) + result.stdout.fnmatch_lines(["*: Trio fixtures can only be used by Trio tests*"]) def test_trio_fixture_with_wrong_scope_without_trio_mode(testdir): @@ -114,9 +112,7 @@ def test_whatever(async_fixture): result = testdir.runpytest() result.assert_outcomes(errors=1) - result.stdout.fnmatch_lines( - ["*: Trio fixtures can only be used by Trio tests*"] - ) + result.stdout.fnmatch_lines(["*: Trio fixtures can only be used by Trio tests*"]) @enable_trio_mode @@ -168,6 +164,4 @@ async def test_whatever(mock_clock, extra_clock): result = testdir.runpytest() result.assert_outcomes(failed=1) - result.stdout.fnmatch_lines( - ["*ValueError: too many clocks spoil the broth!*"] - ) + result.stdout.fnmatch_lines(["*ValueError: too many clocks spoil the broth!*"]) diff --git a/pytest_trio/_tests/test_fixture_nursery.py b/pytest_trio/_tests/test_fixture_nursery.py index 7719fa1..503ad5e 100644 --- a/pytest_trio/_tests/test_fixture_nursery.py +++ b/pytest_trio/_tests/test_fixture_nursery.py @@ -17,6 +17,6 @@ async def server(nursery): @pytest.mark.trio async def test_try(server): stream = await trio.testing.open_stream_to_socket_listener(server) - await stream.send_all(b'ping') + await stream.send_all(b"ping") rep = await stream.receive_some(4) - assert rep == b'ping' + assert rep == b"ping" diff --git a/pytest_trio/_tests/test_fixture_ordering.py b/pytest_trio/_tests/test_fixture_ordering.py index 8af1cdb..c892cd8 100644 --- a/pytest_trio/_tests/test_fixture_ordering.py +++ b/pytest_trio/_tests/test_fixture_ordering.py @@ -283,7 +283,9 @@ def test_post(): "userfix": float("inf"), } assert final_time == 1 - """.replace("CRASHYFIX_HERE", crashyfix) + """.replace( + "CRASHYFIX_HERE", crashyfix + ) ) result = testdir.runpytest() @@ -329,9 +331,7 @@ async def test_try(fixture): result = testdir.runpytest() result.assert_outcomes(passed=0, failed=1) - result.stdout.fnmatch_lines_random([ - "*OOPS*", - ]) + result.stdout.fnmatch_lines_random(["*OOPS*"]) # Makes sure that diff --git a/pytest_trio/_tests/test_hypothesis_interaction.py b/pytest_trio/_tests/test_hypothesis_interaction.py index 30a37db..75aa9f7 100644 --- a/pytest_trio/_tests/test_hypothesis_interaction.py +++ b/pytest_trio/_tests/test_hypothesis_interaction.py @@ -1,8 +1,9 @@ import pytest import trio from trio.tests.test_scheduler_determinism import ( - scheduler_trace, test_the_trio_scheduler_is_not_deterministic, - test_the_trio_scheduler_is_deterministic_if_seeded + scheduler_trace, + test_the_trio_scheduler_is_not_deterministic, + test_the_trio_scheduler_is_deterministic_if_seeded, ) from hypothesis import given, settings, strategies as st @@ -29,7 +30,7 @@ async def test_mark_outer(n): @our_settings -@pytest.mark.parametrize('y', [1, 2]) +@pytest.mark.parametrize("y", [1, 2]) @given(x=st.none()) @pytest.mark.trio async def test_mark_and_parametrize(x, y): diff --git a/pytest_trio/_tests/test_sync_fixture.py b/pytest_trio/_tests/test_sync_fixture.py index 7a4cb0f..508b2f0 100644 --- a/pytest_trio/_tests/test_sync_fixture.py +++ b/pytest_trio/_tests/test_sync_fixture.py @@ -3,12 +3,12 @@ @pytest.fixture def sync_fix(): - return 'sync_fix' + return "sync_fix" @pytest.mark.trio async def test_single_sync_fixture(sync_fix): - assert sync_fix == 'sync_fix' + assert sync_fix == "sync_fix" def test_single_yield_fixture(testdir): @@ -139,6 +139,4 @@ def test_after(): result = testdir.runpytest() result.assert_outcomes(failed=1, passed=2) - result.stdout.re_match_lines( - [r'E\W+RuntimeError: Crash during fixture teardown'] - ) + result.stdout.re_match_lines([r"E\W+RuntimeError: Crash during fixture teardown"]) diff --git a/pytest_trio/_tests/test_trio_mode.py b/pytest_trio/_tests/test_trio_mode.py index f7bf61f..19efdc1 100644 --- a/pytest_trio/_tests/test_trio_mode.py +++ b/pytest_trio/_tests/test_trio_mode.py @@ -56,9 +56,7 @@ def run(*args, **kwargs): def test_trio_mode_and_qtrio_run_configuration(testdir): - testdir.makefile( - ".ini", pytest="[pytest]\ntrio_mode = true\ntrio_run = qtrio\n" - ) + testdir.makefile(".ini", pytest="[pytest]\ntrio_mode = true\ntrio_run = qtrio\n") testdir.makepyfile(qtrio=qtrio_text) @@ -141,9 +139,7 @@ async def test(): def test_closest_explicit_run_wins(testdir): - testdir.makefile( - ".ini", pytest=f"[pytest]\ntrio_mode = true\ntrio_run = trio\n" - ) + testdir.makefile(".ini", pytest=f"[pytest]\ntrio_mode = true\ntrio_run = trio\n") testdir.makepyfile(qtrio=qtrio_text) test_text = """ @@ -163,9 +159,7 @@ async def test(): def test_ini_run_wins_with_blank_marker(testdir): - testdir.makefile( - ".ini", pytest=f"[pytest]\ntrio_mode = true\ntrio_run = qtrio\n" - ) + testdir.makefile(".ini", pytest=f"[pytest]\ntrio_mode = true\ntrio_run = qtrio\n") testdir.makepyfile(qtrio=qtrio_text) test_text = """ diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 986ee52..bcb6528 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -11,8 +11,11 @@ from trio.abc import Clock, Instrument from trio.testing import MockClock from async_generator import ( - async_generator, yield_, asynccontextmanager, isasyncgen, - isasyncgenfunction + async_generator, + yield_, + asynccontextmanager, + isasyncgen, + isasyncgenfunction, ) ################################################################ @@ -51,9 +54,8 @@ def pytest_addoption(parser): def pytest_configure(config): # So that it shows up in 'pytest --markers' output: config.addinivalue_line( - "markers", "trio: " - "mark the test as an async trio test; " - "it will be run using trio.run" + "markers", + "trio: mark the test as an async trio test; it will be run using trio.run", ) @@ -61,7 +63,7 @@ def pytest_configure(config): def pytest_exception_interact(node, call, report): if issubclass(call.excinfo.type, trio.MultiError): # TODO: not really elegant (pytest cannot output color with this hack) - report.longrepr = ''.join(format_exception(*call.excinfo._excinfo)) + report.longrepr = "".join(format_exception(*call.excinfo._excinfo)) ################################################################ @@ -349,13 +351,11 @@ def wrapper(**kwargs): elif len(clocks) == 1: clock = list(clocks.values())[0] else: - raise ValueError(f"Expected at most one Clock in kwargs, got {clocks!r}") - instruments = [ - i for i in kwargs.values() if isinstance(i, Instrument) - ] - return run( - partial(fn, **kwargs), clock=clock, instruments=instruments - ) + raise ValueError( + f"Expected at most one Clock in kwargs, got {clocks!r}" + ) + instruments = [i for i in kwargs.values() if isinstance(i, Instrument)] + return run(partial(fn, **kwargs), clock=clock, instruments=instruments) return wrapper @@ -369,7 +369,7 @@ def _trio_test_runner_factory(item, testfunc=None): testfunc = item.obj for marker in item.iter_markers("trio"): - maybe_run = marker.kwargs.get('run') + maybe_run = marker.kwargs.get("run") if maybe_run is not None: run = maybe_run break @@ -377,15 +377,13 @@ def _trio_test_runner_factory(item, testfunc=None): # no marker found that explicitly specifiers the runner so use config run = choose_run(config=item.config) - if getattr(testfunc, '_trio_test_runner_wrapped', False): + if getattr(testfunc, "_trio_test_runner_wrapped", False): # We have already wrapped this, perhaps because we combined Hypothesis # with pytest.mark.parametrize return testfunc if not iscoroutinefunction(testfunc): - pytest.fail( - 'test function `%r` is marked trio but is not async' % item - ) + pytest.fail("test function `%r` is marked trio but is not async" % item) @_trio_test(run=run) async def _bootstrap_fixtures_and_run_test(**kwargs): @@ -393,10 +391,7 @@ async def _bootstrap_fixtures_and_run_test(**kwargs): test_ctx = TrioTestContext() test = TrioFixture( - "".format(testfunc.__name__), - testfunc, - kwargs, - is_test=True + "".format(testfunc.__name__), testfunc, kwargs, is_test=True ) contextvars_ctx = contextvars.copy_context() @@ -435,16 +430,15 @@ async def _bootstrap_fixtures_and_run_test(**kwargs): @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(item): if item.get_closest_marker("trio") is not None: - if hasattr(item.obj, 'hypothesis'): + if hasattr(item.obj, "hypothesis"): # If it's a Hypothesis test, we go in a layer. item.obj.hypothesis.inner_test = _trio_test_runner_factory( item, item.obj.hypothesis.inner_test ) - elif getattr(item.obj, 'is_hypothesis_test', - False): # pragma: no cover + elif getattr(item.obj, "is_hypothesis_test", False): # pragma: no cover pytest.fail( - 'test function `%r` is using Hypothesis, but pytest-trio ' - 'only works with Hypothesis 3.64.0 or later.' % item + "test function `%r` is using Hypothesis, but pytest-trio " + "only works with Hypothesis 3.64.0 or later." % item ) else: item.obj = _trio_test_runner_factory(item) @@ -463,8 +457,7 @@ def trio_fixture(func): def _is_trio_fixture(func, coerce_async, kwargs): if getattr(func, "_force_trio_fixture", False): return True - if (coerce_async and - (iscoroutinefunction(func) or isasyncgenfunction(func))): + if coerce_async and (iscoroutinefunction(func) or isasyncgenfunction(func)): return True if any(isinstance(value, TrioFixture) for value in kwargs.values()): return True @@ -472,16 +465,13 @@ def _is_trio_fixture(func, coerce_async, kwargs): def handle_fixture(fixturedef, request, force_trio_mode): - is_trio_test = (request.node.get_closest_marker("trio") is not None) + is_trio_test = request.node.get_closest_marker("trio") is not None if force_trio_mode: is_trio_mode = True else: is_trio_mode = request.node.config.getini("trio_mode") - coerce_async = (is_trio_test or is_trio_mode) - kwargs = { - name: request.getfixturevalue(name) - for name in fixturedef.argnames - } + coerce_async = is_trio_test or is_trio_mode + kwargs = {name: request.getfixturevalue(name) for name in fixturedef.argnames} if _is_trio_fixture(fixturedef.func, coerce_async, kwargs): if request.scope != "function": raise RuntimeError("Trio fixtures must be function-scope") @@ -522,11 +512,12 @@ def choose_run(config): run = trio.run elif run_string == "qtrio": import qtrio + run = qtrio.run else: raise ValueError( - f"{run_string!r} not valid for 'trio_run' config." + - " Must be one of: trio, qtrio" + f"{run_string!r} not valid for 'trio_run' config." + + " Must be one of: trio, qtrio" ) return run diff --git a/setup.py b/setup.py index b79a918..7d1f956 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ author_email="emmanuel.leblond@gmail.com", license="MIT -or- Apache License 2.0", packages=find_packages(), - entry_points={'pytest11': ['trio = pytest_trio.plugin']}, + entry_points={"pytest11": ["trio = pytest_trio.plugin"]}, install_requires=[ "trio >= 0.22.0", # for ExceptionGroup support "async_generator >= 1.9", @@ -22,10 +22,10 @@ "pytest >= 7.2.0", # for ExceptionGroup support ], keywords=[ - 'async', - 'pytest', - 'testing', - 'trio', + "async", + "pytest", + "testing", + "trio", ], python_requires=">=3.7", classifiers=[ From 4c2a82f5499cbde7ba041bab4eab16d80bd25350 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sun, 30 Oct 2022 01:11:24 -0700 Subject: [PATCH 169/185] Drop dependency on async_generator --- docs-requirements.in | 1 - docs-requirements.txt | 4 +- docs/source/quickstart.rst | 18 ---- pytest.ini | 4 +- .../_tests/test_async_yield_fixture.py | 102 +++++------------- pytest_trio/_tests/test_fixture_mistakes.py | 8 +- pytest_trio/_tests/test_fixture_ordering.py | 29 ++--- pytest_trio/plugin.py | 14 +-- setup.py | 1 - 9 files changed, 43 insertions(+), 138 deletions(-) diff --git a/docs-requirements.in b/docs-requirements.in index 4e295c0..4acf894 100644 --- a/docs-requirements.in +++ b/docs-requirements.in @@ -10,6 +10,5 @@ towncrier != 19.9.0,!= 21.3.0 # pytest-trio's own dependencies trio >= 0.22.0 -async_generator >= 1.9 outcome >= 1.1.0 pytest >= 7.2.0 diff --git a/docs-requirements.txt b/docs-requirements.txt index b81aba4..ae0c01d 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -7,9 +7,7 @@ alabaster==0.7.12 # via sphinx async-generator==1.10 - # via - # -r docs-requirements.in - # trio + # via trio attrs==22.1.0 # via # -r docs-requirements.in diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst index 490cc12..fe23cd1 100644 --- a/docs/source/quickstart.rst +++ b/docs/source/quickstart.rst @@ -93,17 +93,6 @@ And we're done! Let's try running pytest again: ============================== FAILURES ============================== __________________________ test_should_fail __________________________ - value = - - async def yield_(value=None): - > return await _yield_(value) - - venv/lib/python3.8/site-packages/async_generator/_impl.py:106: - _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - venv/lib/python3.8/site-packages/async_generator/_impl.py:99: in _yield_ - return (yield _wrap(value)) - _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - async def test_should_fail(): > assert False E assert False @@ -179,13 +168,6 @@ regular pytest fixture:: # Teardown code, executed after the test is done await connection.execute("ROLLBACK") -If you need to support Python 3.5, which doesn't allow ``yield`` -inside an ``async def`` function, then you can define async fixtures -using the `async_generator -`__ -library - just make sure to put the ``@pytest.fixture`` *above* the -``@async_generator``. - .. _server-fixture-example: diff --git a/pytest.ini b/pytest.ini index e7e9beb..54951e1 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,2 @@ -[pytest] -addopts = -W error -ra -v --pyargs pytest_trio --verbose --cov +[pytest] +addopts = -W error -ra -v --pyargs pytest_trio --verbose --cov diff --git a/pytest_trio/_tests/test_async_yield_fixture.py b/pytest_trio/_tests/test_async_yield_fixture.py index 6eee232..58d1a55 100644 --- a/pytest_trio/_tests/test_async_yield_fixture.py +++ b/pytest_trio/_tests/test_async_yield_fixture.py @@ -1,40 +1,17 @@ -import sys -import pytest -import re - - -@pytest.fixture(params=["Python>=36", "async_generator"]) -def async_yield_implementation(request): - if request.param == "Python>=36": - - def patch_code(code): - # Convert code to use Python>=3.6 builtin async generator - code = re.sub(r"(?m)^\s*@async_generator\n", r"", code) - code = re.sub(r"await yield_", r"yield", code) - return code - - return patch_code - else: - return lambda x: x - - -def test_single_async_yield_fixture(testdir, async_yield_implementation): +def test_single_async_yield_fixture(testdir): testdir.makepyfile( - async_yield_implementation( - """ + """ import pytest import trio - from async_generator import async_generator, yield_ events = [] @pytest.fixture - @async_generator async def fix1(): events.append('fix1 setup') await trio.sleep(0) - await yield_('fix1') + yield 'fix1' await trio.sleep(0) events.append('fix1 teardown') @@ -53,7 +30,6 @@ def test_after(): 'fix1 teardown', ] """ - ) ) result = testdir.runpytest() @@ -61,35 +37,31 @@ def test_after(): result.assert_outcomes(passed=3) -def test_nested_async_yield_fixture(testdir, async_yield_implementation): +def test_nested_async_yield_fixture(testdir): testdir.makepyfile( - async_yield_implementation( - """ + """ import pytest import trio - from async_generator import async_generator, yield_ events = [] @pytest.fixture - @async_generator async def fix2(): events.append('fix2 setup') await trio.sleep(0) - await yield_('fix2') + yield 'fix2' await trio.sleep(0) events.append('fix2 teardown') @pytest.fixture - @async_generator async def fix1(fix2): events.append('fix1 setup') await trio.sleep(0) - await yield_('fix1') + yield 'fix1' await trio.sleep(0) events.append('fix1 teardown') @@ -113,7 +85,6 @@ def test_after(): 'fix2 teardown', ] """ - ) ) result = testdir.runpytest() @@ -121,24 +92,21 @@ def test_after(): result.assert_outcomes(passed=3) -def test_async_yield_fixture_within_sync_fixture(testdir, async_yield_implementation): +def test_async_yield_fixture_within_sync_fixture(testdir): testdir.makepyfile( - async_yield_implementation( - """ + """ import pytest import trio - from async_generator import async_generator, yield_ events = [] @pytest.fixture - @async_generator async def fix2(): events.append('fix2 setup') await trio.sleep(0) - await yield_('fix2') + yield 'fix2' await trio.sleep(0) events.append('fix2 teardown') @@ -163,7 +131,6 @@ def test_after(): 'fix2 teardown', ] """ - ) ) result = testdir.runpytest() @@ -171,26 +138,21 @@ def test_after(): result.assert_outcomes(passed=3) -def test_async_yield_fixture_within_sync_yield_fixture( - testdir, async_yield_implementation -): +def test_async_yield_fixture_within_sync_yield_fixture(testdir): testdir.makepyfile( - async_yield_implementation( - """ + """ import pytest import trio - from async_generator import async_generator, yield_ events = [] @pytest.fixture - @async_generator async def fix2(): events.append('fix2 setup') await trio.sleep(0) - await yield_('fix2') + yield 'fix2' await trio.sleep(0) events.append('fix2 teardown') @@ -220,7 +182,6 @@ def test_after(): 'fix2 teardown', ] """ - ) ) result = testdir.runpytest() @@ -228,28 +189,24 @@ def test_after(): result.assert_outcomes(passed=3) -def test_async_yield_fixture_with_multiple_yields(testdir, async_yield_implementation): +def test_async_yield_fixture_with_multiple_yields(testdir): testdir.makepyfile( - async_yield_implementation( - """ + """ import pytest import trio - from async_generator import async_generator, yield_ @pytest.fixture - @async_generator async def fix1(): await trio.sleep(0) - await yield_('good') + yield 'good' await trio.sleep(0) - await yield_('bad') + yield 'bad' @pytest.mark.trio async def test_actual_test(fix1): pass """ - ) ) result = testdir.runpytest() @@ -259,14 +216,12 @@ async def test_actual_test(fix1): result.assert_outcomes(failed=1) -def test_async_yield_fixture_with_nursery(testdir, async_yield_implementation): +def test_async_yield_fixture_with_nursery(testdir): testdir.makepyfile( - async_yield_implementation( - """ + """ import pytest import trio - from async_generator import async_generator, yield_ async def handle_client(stream): @@ -276,11 +231,10 @@ async def handle_client(stream): @pytest.fixture - @async_generator async def server(): async with trio.open_nursery() as nursery: listeners = await nursery.start(trio.serve_tcp, handle_client, 0) - await yield_(listeners[0]) + yield listeners[0] nursery.cancel_scope.cancel() @@ -291,7 +245,6 @@ async def test_actual_test(server): rep = await stream.receive_some(4) assert rep == b'ping' """ - ) ) result = testdir.runpytest() @@ -299,34 +252,28 @@ async def test_actual_test(server): result.assert_outcomes(passed=1) -def test_async_yield_fixture_crashed_teardown_allow_other_teardowns( - testdir, async_yield_implementation -): +def test_async_yield_fixture_crashed_teardown_allow_other_teardowns(testdir): testdir.makepyfile( - async_yield_implementation( - """ + """ import pytest import trio - from async_generator import async_generator, yield_ setup_events = set() teardown_events = set() @pytest.fixture - @async_generator async def good_fixture(): async with trio.open_nursery() as nursery: setup_events.add('good_fixture setup') - await yield_(None) + yield None teardown_events.add('good_fixture teardown') @pytest.fixture - @async_generator async def bad_fixture(): async with trio.open_nursery() as nursery: setup_events.add('bad_fixture setup') - await yield_(None) + yield None teardown_events.add('bad_fixture teardown') raise RuntimeError('Crash during fixture teardown') @@ -348,7 +295,6 @@ def test_after(): 'good_fixture teardown', } """ - ) ) result = testdir.runpytest() diff --git a/pytest_trio/_tests/test_fixture_mistakes.py b/pytest_trio/_tests/test_fixture_mistakes.py index b00de9f..7bd38ed 100644 --- a/pytest_trio/_tests/test_fixture_mistakes.py +++ b/pytest_trio/_tests/test_fixture_mistakes.py @@ -123,14 +123,12 @@ def test_fixture_cancels_test_but_doesnt_raise(testdir, enable_trio_mode): """ import pytest import trio - from async_generator import async_generator, yield_ @pytest.fixture - @async_generator async def async_fixture(): with trio.CancelScope() as cscope: cscope.cancel() - await yield_() + yield async def test_whatever(async_fixture): @@ -164,4 +162,6 @@ async def test_whatever(mock_clock, extra_clock): result = testdir.runpytest() result.assert_outcomes(failed=1) - result.stdout.fnmatch_lines(["*ValueError: too many clocks spoil the broth!*"]) + result.stdout.fnmatch_lines( + ["*ValueError: Expected at most one Clock in kwargs, got *"] + ) diff --git a/pytest_trio/_tests/test_fixture_ordering.py b/pytest_trio/_tests/test_fixture_ordering.py index c892cd8..a180958 100644 --- a/pytest_trio/_tests/test_fixture_ordering.py +++ b/pytest_trio/_tests/test_fixture_ordering.py @@ -11,7 +11,6 @@ def test_fixture_basic_ordering(testdir): import pytest from pytest_trio import trio_fixture from trio.testing import Sequencer - from async_generator import async_generator, yield_ setup_events = [] teardown_events = [] @@ -21,10 +20,9 @@ def seq(): return Sequencer() @pytest.fixture - @async_generator async def leaf_fix(): setup_events.append("leaf_fix setup") - await yield_() + yield teardown_events.append("leaf_fix teardown") assert teardown_events == [ @@ -36,26 +34,24 @@ async def leaf_fix(): ] @pytest.fixture - @async_generator async def fix_concurrent_1(leaf_fix, seq): async with seq(0): setup_events.append("fix_concurrent_1 setup 1") async with seq(2): setup_events.append("fix_concurrent_1 setup 2") - await yield_() + yield async with seq(4): teardown_events.append("fix_concurrent_1 teardown 1") async with seq(6): teardown_events.append("fix_concurrent_1 teardown 2") @pytest.fixture - @async_generator async def fix_concurrent_2(leaf_fix, seq): async with seq(1): setup_events.append("fix_concurrent_2 setup 1") async with seq(3): setup_events.append("fix_concurrent_2 setup 2") - await yield_() + yield async with seq(5): teardown_events.append("fix_concurrent_2 teardown 1") async with seq(7): @@ -148,7 +144,6 @@ def test_error_collection(testdir): import pytest from pytest_trio import trio_fixture import trio - from async_generator import async_generator, yield_ test_started = False @@ -159,17 +154,15 @@ async def crash_nongen(): raise RuntimeError("crash_nongen".upper()) @trio_fixture - @async_generator async def crash_early_agen(): with trio.CancelScope(shield=True): await trio.sleep(2) raise RuntimeError("crash_early_agen".upper()) - await yield_() + yield @trio_fixture - @async_generator async def crash_late_agen(): - await yield_() + yield raise RuntimeError("crash_late_agen".upper()) async def crash(when, token): @@ -223,12 +216,11 @@ def crashyfix(nursery): crashyfix_using_manual_nursery = """ @trio_fixture - @async_generator async def crashyfix(): async with trio.open_nursery() as nursery: nursery.start_soon(crashy) with pytest.raises(trio.Cancelled): - await yield_() + yield # We should be cancelled here teardown_deadlines["crashyfix"] = trio.current_effective_deadline() """ @@ -243,7 +235,6 @@ async def crashyfix(): import pytest from pytest_trio import trio_fixture import trio - from async_generator import async_generator, yield_ teardown_deadlines = {} final_time = None @@ -300,27 +291,25 @@ def test_complex_cancel_interaction_regression(testdir): """ import pytest import trio - from async_generator import asynccontextmanager, async_generator, yield_ + from contextlib import asynccontextmanager async def die_soon(): raise RuntimeError('oops'.upper()) @asynccontextmanager - @async_generator async def async_finalizer(): try: - await yield_() + yield finally: await trio.sleep(0) @pytest.fixture - @async_generator async def fixture(nursery): async with trio.open_nursery() as nursery1: async with async_finalizer(): async with trio.open_nursery() as nursery2: nursery2.start_soon(die_soon) - await yield_() + yield nursery1.cancel_scope.cancel() @pytest.mark.trio diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index bcb6528..34e64a8 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -1,22 +1,15 @@ """pytest-trio implementation.""" from functools import wraps, partial -import sys from traceback import format_exception from collections.abc import Coroutine, Generator -from inspect import iscoroutinefunction, isgeneratorfunction +from contextlib import asynccontextmanager +from inspect import isasyncgen, isasyncgenfunction, iscoroutinefunction import contextvars import outcome import pytest import trio from trio.abc import Clock, Instrument from trio.testing import MockClock -from async_generator import ( - async_generator, - yield_, - asynccontextmanager, - isasyncgen, - isasyncgenfunction, -) ################################################################ # Basic setup @@ -197,13 +190,12 @@ def register_and_collect_dependencies(self): return deps @asynccontextmanager - @async_generator async def _fixture_manager(self, test_ctx): __tracebackhide__ = True try: async with trio.open_nursery() as nursery_fixture: try: - await yield_(nursery_fixture) + yield nursery_fixture finally: nursery_fixture.cancel_scope.cancel() except BaseException as exc: diff --git a/setup.py b/setup.py index 7d1f956..ecf6f8b 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,6 @@ entry_points={"pytest11": ["trio = pytest_trio.plugin"]}, install_requires=[ "trio >= 0.22.0", # for ExceptionGroup support - "async_generator >= 1.9", "outcome >= 1.1.0", "pytest >= 7.2.0", # for ExceptionGroup support ], From ce850d6070a345dcef232aae815b4aa067e25e0a Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sun, 30 Oct 2022 01:11:24 -0700 Subject: [PATCH 170/185] Create 129.misc.rst --- newsfragments/129.misc.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 newsfragments/129.misc.rst diff --git a/newsfragments/129.misc.rst b/newsfragments/129.misc.rst new file mode 100644 index 0000000..13b403f --- /dev/null +++ b/newsfragments/129.misc.rst @@ -0,0 +1,2 @@ +Dropped support for end-of-life Python 3.6, and the ``async_generator`` library +necessary to support it, and started testing on Python 3.10 and 3.11. From f788fe7d1bc7e7cbfa8e86d482018afd785536d1 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sun, 30 Oct 2022 01:11:24 -0700 Subject: [PATCH 171/185] TEMP: ignore warnings --- pytest.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pytest.ini b/pytest.ini index 54951e1..92c555f 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,2 @@ -[pytest] -addopts = -W error -ra -v --pyargs pytest_trio --verbose --cov +[pytest] +addopts = -ra -v --pyargs pytest_trio --verbose --cov From ed732d31cd501bcc3edd97268de2c67cd3a3e255 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Mon, 31 Oct 2022 10:31:13 -0700 Subject: [PATCH 172/185] Remove use of MultiError --- newsfragments/128.misc.rst | 4 ++++ .../_tests/test_async_yield_fixture.py | 4 +++- pytest_trio/_tests/test_sync_fixture.py | 4 +++- pytest_trio/plugin.py | 20 +++++++++---------- 4 files changed, 20 insertions(+), 12 deletions(-) create mode 100644 newsfragments/128.misc.rst diff --git a/newsfragments/128.misc.rst b/newsfragments/128.misc.rst new file mode 100644 index 0000000..dacb961 --- /dev/null +++ b/newsfragments/128.misc.rst @@ -0,0 +1,4 @@ +Trio 0.22.0 deprecated ``MultiError`` in favor of the standard-library +(or `backported `__) ``ExceptionGroup`` +type; ``pytest-trio`` now uses ``ExceptionGroup`` and therefore requires +Trio 0.22.0 or later. diff --git a/pytest_trio/_tests/test_async_yield_fixture.py b/pytest_trio/_tests/test_async_yield_fixture.py index 58d1a55..ca99159 100644 --- a/pytest_trio/_tests/test_async_yield_fixture.py +++ b/pytest_trio/_tests/test_async_yield_fixture.py @@ -300,4 +300,6 @@ def test_after(): result = testdir.runpytest() result.assert_outcomes(failed=1, passed=2) - result.stdout.re_match_lines([r"E\W+RuntimeError: Crash during fixture teardown"]) + result.stdout.re_match_lines( + [r"(E\W+| +\| )RuntimeError: Crash during fixture teardown"] + ) diff --git a/pytest_trio/_tests/test_sync_fixture.py b/pytest_trio/_tests/test_sync_fixture.py index 508b2f0..738ffbc 100644 --- a/pytest_trio/_tests/test_sync_fixture.py +++ b/pytest_trio/_tests/test_sync_fixture.py @@ -139,4 +139,6 @@ def test_after(): result = testdir.runpytest() result.assert_outcomes(failed=1, passed=2) - result.stdout.re_match_lines([r"E\W+RuntimeError: Crash during fixture teardown"]) + result.stdout.re_match_lines( + [r"(E\W+| +\| )RuntimeError: Crash during fixture teardown"] + ) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 34e64a8..216f00c 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -1,6 +1,6 @@ """pytest-trio implementation.""" +import sys from functools import wraps, partial -from traceback import format_exception from collections.abc import Coroutine, Generator from contextlib import asynccontextmanager from inspect import isasyncgen, isasyncgenfunction, iscoroutinefunction @@ -11,6 +11,9 @@ from trio.abc import Clock, Instrument from trio.testing import MockClock +if sys.version_info[:2] < (3, 11): + from exceptiongroup import BaseExceptionGroup + ################################################################ # Basic setup ################################################################ @@ -52,13 +55,6 @@ def pytest_configure(config): ) -@pytest.hookimpl(tryfirst=True) -def pytest_exception_interact(node, call, report): - if issubclass(call.excinfo.type, trio.MultiError): - # TODO: not really elegant (pytest cannot output color with this hack) - report.longrepr = "".join(format_exception(*call.excinfo._excinfo)) - - ################################################################ # Core support for trio fixtures and trio tests ################################################################ @@ -407,8 +403,12 @@ async def _bootstrap_fixtures_and_run_test(**kwargs): ) ) - if test_ctx.error_list: - raise trio.MultiError(test_ctx.error_list) + if len(test_ctx.error_list) == 1: + raise test_ctx.error_list[0] + elif test_ctx.error_list: + raise BaseExceptionGroup( + "errors in async test and trio fixtures", test_ctx.error_list + ) _bootstrap_fixtures_and_run_test._trio_test_runner_wrapped = True return _bootstrap_fixtures_and_run_test From 648f9e923e509a9a9ce9590365d09fd5c2ebb4e5 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Mon, 31 Oct 2022 10:31:13 -0700 Subject: [PATCH 173/185] Unwrap magic exceptions from single-leaf groups --- newsfragments/104.feature.rst | 10 +++++++ pytest_trio/_tests/test_basic.py | 49 ++++++++++++++++++++++++++++++++ pytest_trio/plugin.py | 21 +++++++++++++- 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 newsfragments/104.feature.rst diff --git a/newsfragments/104.feature.rst b/newsfragments/104.feature.rst new file mode 100644 index 0000000..19e9f2f --- /dev/null +++ b/newsfragments/104.feature.rst @@ -0,0 +1,10 @@ +If a test raises an ``ExceptionGroup`` (or nested ``ExceptionGroup``\ s) with only +a single 'leaf' exception from ``pytest.xfail()`` or ``pytest.skip()``\ , we now +unwrap it to have the desired effect on Pytest. ``ExceptionGroup``\ s with two or +more leaf exceptions, even of the same type, are not changed and will be treated +as ordinary test failures. + +See `pytest-dev/pytest#9680 `__ +for design discussion. This feature is particularly useful if you've enabled +`the new strict_exception_groups=True option +`__. diff --git a/pytest_trio/_tests/test_basic.py b/pytest_trio/_tests/test_basic.py index 65dd911..9702498 100644 --- a/pytest_trio/_tests/test_basic.py +++ b/pytest_trio/_tests/test_basic.py @@ -74,3 +74,52 @@ def test_invalid(): result = testdir.runpytest() result.assert_outcomes(errors=1) + + +def test_skip_and_xfail(testdir): + + testdir.makepyfile( + """ + import functools + import pytest + import trio + + trio.run = functools.partial(trio.run, strict_exception_groups=True) + + @pytest.mark.trio + async def test_xfail(): + pytest.xfail() + + @pytest.mark.trio + async def test_skip(): + pytest.skip() + + async def callback(fn): + fn() + + async def fail(): + raise RuntimeError + + @pytest.mark.trio + async def test_xfail_and_fail(): + async with trio.open_nursery() as nursery: + nursery.start_soon(callback, pytest.xfail) + nursery.start_soon(fail) + + @pytest.mark.trio + async def test_skip_and_fail(): + async with trio.open_nursery() as nursery: + nursery.start_soon(callback, pytest.skip) + nursery.start_soon(fail) + + @pytest.mark.trio + async def test_xfail_and_skip(): + async with trio.open_nursery() as nursery: + nursery.start_soon(callback, pytest.skip) + nursery.start_soon(callback, pytest.xfail) + """ + ) + + result = testdir.runpytest("-s") + + result.assert_outcomes(skipped=1, xfailed=1, failed=3) diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 216f00c..6638d67 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -10,6 +10,7 @@ import trio from trio.abc import Clock, Instrument from trio.testing import MockClock +from _pytest.outcomes import Skipped, XFailed if sys.version_info[:2] < (3, 11): from exceptiongroup import BaseExceptionGroup @@ -343,7 +344,25 @@ def wrapper(**kwargs): f"Expected at most one Clock in kwargs, got {clocks!r}" ) instruments = [i for i in kwargs.values() if isinstance(i, Instrument)] - return run(partial(fn, **kwargs), clock=clock, instruments=instruments) + try: + return run(partial(fn, **kwargs), clock=clock, instruments=instruments) + except BaseExceptionGroup as eg: + queue = [eg] + leaves = [] + while queue: + ex = queue.pop() + if isinstance(ex, BaseExceptionGroup): + queue.extend(ex.exceptions) + else: + leaves.append(ex) + if len(leaves) == 1: + if isinstance(leaves[0], XFailed): + pytest.xfail() + if isinstance(leaves[0], Skipped): + pytest.skip() + # Since our leaf exceptions don't consist of exactly one 'magic' + # skipped or xfailed exception, re-raise the whole group. + raise return wrapper From 6fbe63eaf84c81fd11607c88b973741b4be42a39 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Mon, 31 Oct 2022 10:31:13 -0700 Subject: [PATCH 174/185] Adjust warnings config --- pytest.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pytest.ini b/pytest.ini index 92c555f..67ed440 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,2 +1,7 @@ [pytest] addopts = -ra -v --pyargs pytest_trio --verbose --cov +filterwarnings = + error + default::pytest.PytestAssertRewriteWarning + default::pytest.PytestDeprecationWarning + default::pytest.PytestUnraisableExceptionWarning From 250ec45f5d72eca78ab0ea2d1571667d2c51f8b5 Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Tue, 1 Nov 2022 21:13:47 +0400 Subject: [PATCH 175/185] Bump version to 0.8.0 --- docs/source/history.rst | 38 +++++++++++++++++++++++++++++++++++ newsfragments/104.feature.rst | 10 --------- newsfragments/120.bugfix.rst | 3 --- newsfragments/128.misc.rst | 4 ---- newsfragments/129.misc.rst | 2 -- pytest_trio/_version.py | 2 +- 6 files changed, 39 insertions(+), 20 deletions(-) delete mode 100644 newsfragments/104.feature.rst delete mode 100644 newsfragments/120.bugfix.rst delete mode 100644 newsfragments/128.misc.rst delete mode 100644 newsfragments/129.misc.rst diff --git a/docs/source/history.rst b/docs/source/history.rst index 9ef63e5..c02178a 100644 --- a/docs/source/history.rst +++ b/docs/source/history.rst @@ -5,6 +5,44 @@ Release history .. towncrier release notes start +pytest-trio 0.8.0 (2022-11-01) +------------------------------ + +Features +~~~~~~~~ + +- If a test raises an ``ExceptionGroup`` (or nested ``ExceptionGroup``\ s) with only + a single 'leaf' exception from ``pytest.xfail()`` or ``pytest.skip()``\ , we now + unwrap it to have the desired effect on Pytest. ``ExceptionGroup``\ s with two or + more leaf exceptions, even of the same type, are not changed and will be treated + as ordinary test failures. + + See `pytest-dev/pytest#9680 `__ + for design discussion. This feature is particularly useful if you've enabled + `the new strict_exception_groups=True option + `__. (`#104 `__) + + +Bugfixes +~~~~~~~~ + +- Fix an issue where if two fixtures are being set up concurrently, and + one crashes and the other hangs, then the test as a whole would hang, + rather than being cancelled and unwound after the crash. (`#120 `__) + + +Misc +~~~~ + +- Trio 0.22.0 deprecated ``MultiError`` in favor of the standard-library + (or `backported `__) ``ExceptionGroup`` + type; ``pytest-trio`` now uses ``ExceptionGroup`` exclusively and therefore requires + Trio 0.22.0 or later. (`#128 `__) + +- Dropped support for end-of-life Python 3.6, and the ``async_generator`` library + necessary to support it, and started testing on Python 3.10 and 3.11. (`#129 `__) + + pytest-trio 0.7.0 (2020-10-15) ------------------------------ diff --git a/newsfragments/104.feature.rst b/newsfragments/104.feature.rst deleted file mode 100644 index 19e9f2f..0000000 --- a/newsfragments/104.feature.rst +++ /dev/null @@ -1,10 +0,0 @@ -If a test raises an ``ExceptionGroup`` (or nested ``ExceptionGroup``\ s) with only -a single 'leaf' exception from ``pytest.xfail()`` or ``pytest.skip()``\ , we now -unwrap it to have the desired effect on Pytest. ``ExceptionGroup``\ s with two or -more leaf exceptions, even of the same type, are not changed and will be treated -as ordinary test failures. - -See `pytest-dev/pytest#9680 `__ -for design discussion. This feature is particularly useful if you've enabled -`the new strict_exception_groups=True option -`__. diff --git a/newsfragments/120.bugfix.rst b/newsfragments/120.bugfix.rst deleted file mode 100644 index cd60b41..0000000 --- a/newsfragments/120.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fix an issue where if two fixtures are being set up concurrently, and -one crashes and the other hangs, then the test as a whole would hang, -rather than being cancelled and unwound after the crash. diff --git a/newsfragments/128.misc.rst b/newsfragments/128.misc.rst deleted file mode 100644 index dacb961..0000000 --- a/newsfragments/128.misc.rst +++ /dev/null @@ -1,4 +0,0 @@ -Trio 0.22.0 deprecated ``MultiError`` in favor of the standard-library -(or `backported `__) ``ExceptionGroup`` -type; ``pytest-trio`` now uses ``ExceptionGroup`` and therefore requires -Trio 0.22.0 or later. diff --git a/newsfragments/129.misc.rst b/newsfragments/129.misc.rst deleted file mode 100644 index 13b403f..0000000 --- a/newsfragments/129.misc.rst +++ /dev/null @@ -1,2 +0,0 @@ -Dropped support for end-of-life Python 3.6, and the ``async_generator`` library -necessary to support it, and started testing on Python 3.10 and 3.11. diff --git a/pytest_trio/_version.py b/pytest_trio/_version.py index 9d95beb..af2f871 100644 --- a/pytest_trio/_version.py +++ b/pytest_trio/_version.py @@ -1,3 +1,3 @@ # This file is imported from __init__.py and exec'd from setup.py -__version__ = "0.7.0+dev" +__version__ = "0.8.0" From 6705eb4758db61bd3ed6f1adc188b264f6f2ad9a Mon Sep 17 00:00:00 2001 From: Quentin Pradet Date: Tue, 1 Nov 2022 21:24:50 +0400 Subject: [PATCH 176/185] Bump version to 0.8.0+dev --- pytest_trio/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_trio/_version.py b/pytest_trio/_version.py index af2f871..a149830 100644 --- a/pytest_trio/_version.py +++ b/pytest_trio/_version.py @@ -1,3 +1,3 @@ # This file is imported from __init__.py and exec'd from setup.py -__version__ = "0.8.0" +__version__ = "0.8.0+dev" From 0d3bb9a38eec412439f7150b97424d43fef57b37 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Thu, 3 Nov 2022 14:41:23 -0700 Subject: [PATCH 177/185] Fix compatibility with e.g. Sybil --- newsfragments/132.bugfix.rst | 1 + pytest_trio/plugin.py | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 newsfragments/132.bugfix.rst diff --git a/newsfragments/132.bugfix.rst b/newsfragments/132.bugfix.rst new file mode 100644 index 0000000..4850310 --- /dev/null +++ b/newsfragments/132.bugfix.rst @@ -0,0 +1 @@ +Fix a bad interaction with custom pytest items that do not include an ``.obj`` attribute. diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 6638d67..1a56a83 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -508,6 +508,9 @@ def pytest_fixture_setup(fixturedef, request): def automark(items, run=trio.run): for item in items: + if not hasattr(item, "obj"): + # Rare and a little strange, but happens with some doctest-like plugins + continue if hasattr(item.obj, "hypothesis"): test_func = item.obj.hypothesis.inner_test else: From 9cda20bbb966fe1e4ae51921d566c668654ee5e1 Mon Sep 17 00:00:00 2001 From: Vincent Vanlaer Date: Sun, 3 Sep 2023 00:00:54 +0200 Subject: [PATCH 178/185] Remove trio.tests import causing warnings It is deprecated and the replacement is made private as trio._tests. While we could be using that, this commit copies over the one relevant function that is actually necessary. The other two imports just repeat tests that are already in trio and do not need repeating here. --- .../_tests/test_hypothesis_interaction.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/pytest_trio/_tests/test_hypothesis_interaction.py b/pytest_trio/_tests/test_hypothesis_interaction.py index 75aa9f7..cb95a96 100644 --- a/pytest_trio/_tests/test_hypothesis_interaction.py +++ b/pytest_trio/_tests/test_hypothesis_interaction.py @@ -1,10 +1,5 @@ import pytest import trio -from trio.tests.test_scheduler_determinism import ( - scheduler_trace, - test_the_trio_scheduler_is_not_deterministic, - test_the_trio_scheduler_is_deterministic_if_seeded, -) from hypothesis import given, settings, strategies as st from pytest_trio.plugin import _trio_test_runner_factory @@ -38,6 +33,22 @@ async def test_mark_and_parametrize(x, y): assert y in (1, 2) +async def scheduler_trace(): + """Returns a scheduler-dependent value we can use to check determinism.""" + trace = [] + + async def tracer(name): + for i in range(10): + trace.append((name, i)) + await trio.sleep(0) + + async with trio.open_nursery() as nursery: + for i in range(5): + nursery.start_soon(tracer, i) + + return tuple(trace) + + def test_the_trio_scheduler_is_deterministic_under_hypothesis(): traces = [] From 788fee31d2bc3a9ee659dfce6162b3c0cf9d6a80 Mon Sep 17 00:00:00 2001 From: Vincent Vanlaer Date: Sun, 3 Sep 2023 00:16:47 +0200 Subject: [PATCH 179/185] Fix black changes --- docs/source/conf.py | 1 + pytest_trio/_tests/test_async_fixture.py | 5 ----- pytest_trio/_tests/test_async_yield_fixture.py | 6 ------ pytest_trio/_tests/test_basic.py | 4 ---- pytest_trio/_tests/test_sync_fixture.py | 3 --- 5 files changed, 1 insertion(+), 18 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7a71ac8..ff74f8b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -45,6 +45,7 @@ ("py:obj", "bytes-like"), ] + # XX hack the RTD theme until # https://github.com/rtfd/sphinx_rtd_theme/pull/382 # is shipped (should be in the release after 0.2.4) diff --git a/pytest_trio/_tests/test_async_fixture.py b/pytest_trio/_tests/test_async_fixture.py index 88c4aa9..3703f73 100644 --- a/pytest_trio/_tests/test_async_fixture.py +++ b/pytest_trio/_tests/test_async_fixture.py @@ -2,7 +2,6 @@ def test_single_async_fixture(testdir): - testdir.makepyfile( """ import pytest @@ -25,7 +24,6 @@ async def test_simple(fix1): def test_async_fixture_recomputed_for_each_test(testdir): - testdir.makepyfile( """ import pytest @@ -56,7 +54,6 @@ async def test_second(fix1): def test_nested_async_fixture(testdir): - testdir.makepyfile( """ import pytest @@ -89,7 +86,6 @@ async def test_both(fix1, fix2): def test_async_within_sync_fixture(testdir): - testdir.makepyfile( """ import pytest @@ -122,7 +118,6 @@ async def test_simple(sync_fix): # async fixture... @pytest.mark.xfail(reason="Not implemented yet") def test_raise_in_async_fixture_cause_pytest_error(testdir): - testdir.makepyfile( """ import pytest diff --git a/pytest_trio/_tests/test_async_yield_fixture.py b/pytest_trio/_tests/test_async_yield_fixture.py index ca99159..9505eb2 100644 --- a/pytest_trio/_tests/test_async_yield_fixture.py +++ b/pytest_trio/_tests/test_async_yield_fixture.py @@ -38,7 +38,6 @@ def test_after(): def test_nested_async_yield_fixture(testdir): - testdir.makepyfile( """ import pytest @@ -93,7 +92,6 @@ def test_after(): def test_async_yield_fixture_within_sync_fixture(testdir): - testdir.makepyfile( """ import pytest @@ -139,7 +137,6 @@ def test_after(): def test_async_yield_fixture_within_sync_yield_fixture(testdir): - testdir.makepyfile( """ import pytest @@ -190,7 +187,6 @@ def test_after(): def test_async_yield_fixture_with_multiple_yields(testdir): - testdir.makepyfile( """ import pytest @@ -217,7 +213,6 @@ async def test_actual_test(fix1): def test_async_yield_fixture_with_nursery(testdir): - testdir.makepyfile( """ import pytest @@ -253,7 +248,6 @@ async def test_actual_test(server): def test_async_yield_fixture_crashed_teardown_allow_other_teardowns(testdir): - testdir.makepyfile( """ import pytest diff --git a/pytest_trio/_tests/test_basic.py b/pytest_trio/_tests/test_basic.py index 9702498..f95538f 100644 --- a/pytest_trio/_tests/test_basic.py +++ b/pytest_trio/_tests/test_basic.py @@ -2,7 +2,6 @@ def test_async_test_is_executed(testdir): - testdir.makepyfile( """ import pytest @@ -27,7 +26,6 @@ def test_check_async_test_called(): def test_async_test_as_class_method(testdir): - testdir.makepyfile( """ import pytest @@ -60,7 +58,6 @@ def test_check_async_test_called(): @pytest.mark.xfail(reason="Raises pytest internal error so far...") def test_sync_function_with_trio_mark(testdir): - testdir.makepyfile( """ import pytest @@ -77,7 +74,6 @@ def test_invalid(): def test_skip_and_xfail(testdir): - testdir.makepyfile( """ import functools diff --git a/pytest_trio/_tests/test_sync_fixture.py b/pytest_trio/_tests/test_sync_fixture.py index 738ffbc..c6060a4 100644 --- a/pytest_trio/_tests/test_sync_fixture.py +++ b/pytest_trio/_tests/test_sync_fixture.py @@ -12,7 +12,6 @@ async def test_single_sync_fixture(sync_fix): def test_single_yield_fixture(testdir): - testdir.makepyfile( """ import pytest @@ -47,7 +46,6 @@ def test_after(): def test_single_yield_fixture_with_async_deps(testdir): - testdir.makepyfile( """ import pytest @@ -90,7 +88,6 @@ def test_after(): def test_sync_yield_fixture_crashed_teardown_allow_other_teardowns(testdir): - testdir.makepyfile( """ import pytest From c6e1650f60e0f786e8f1848d07116ac98c982f51 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sun, 14 Jul 2024 13:34:57 -0700 Subject: [PATCH 180/185] Fix typo --- newsfragments/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsfragments/README.rst b/newsfragments/README.rst index 291bfea..4df2975 100644 --- a/newsfragments/README.rst +++ b/newsfragments/README.rst @@ -9,7 +9,7 @@ message and PR description, which are a description of the change as relevant to people working on the code itself.) Each file should be named like ``..rst``, where -```` is an issue numbers, and ```` is one of: +```` is an issue number, and ```` is one of: * ``feature`` * ``bugfix`` From 080bc418bf16ed128dc119067a677e1adffb9607 Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sun, 14 Jul 2024 13:34:57 -0700 Subject: [PATCH 181/185] Upstreamed Hypothesis support --- newsfragments/143.misc.rst | 3 +++ pytest_trio/plugin.py | 15 +-------------- 2 files changed, 4 insertions(+), 14 deletions(-) create mode 100644 newsfragments/143.misc.rst diff --git a/newsfragments/143.misc.rst b/newsfragments/143.misc.rst new file mode 100644 index 0000000..f20319d --- /dev/null +++ b/newsfragments/143.misc.rst @@ -0,0 +1,3 @@ +Move some Hypothesis support into Trio itself, via the new plugins system. +As a result, ``pytest-trio`` will not import Hypothesis, while still +integrating seamlessly if _you_ import it. diff --git a/pytest_trio/plugin.py b/pytest_trio/plugin.py index 1a56a83..ebfcbee 100644 --- a/pytest_trio/plugin.py +++ b/pytest_trio/plugin.py @@ -1,4 +1,5 @@ """pytest-trio implementation.""" + import sys from functools import wraps, partial from collections.abc import Coroutine, Generator @@ -19,20 +20,6 @@ # Basic setup ################################################################ -try: - from hypothesis import register_random -except ImportError: # pragma: no cover - pass -else: - # On recent versions of Hypothesis, make the Trio scheduler deterministic - # even though it uses a module-scoped Random instance. This works - # regardless of whether or not the random_module strategy is used. - register_random(trio._core._run._r) - # We also have to enable determinism, which is disabled by default - # due to a small performance impact - but fine to enable in testing. - # See https://github.com/python-trio/trio/pull/890/ for details. - trio._core._run._ALLOW_DETERMINISTIC_SCHEDULING = True - def pytest_addoption(parser): parser.addini( From 9666949ae08ec30789f72b8a2033ba930968590b Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sun, 14 Jul 2024 13:34:57 -0700 Subject: [PATCH 182/185] Drop py37, add py312 to CI --- .github/workflows/ci.yml | 6 +++--- docs-requirements.in | 2 +- docs-requirements.txt | 13 ++++--------- setup.py | 4 ++-- test-requirements.in | 3 +++ test-requirements.txt | 37 ++++++++++++++++++++++++++++++++++--- 6 files changed, 47 insertions(+), 18 deletions(-) create mode 100644 test-requirements.in diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9544ed..e3cbb9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - name: Checkout uses: actions/checkout@v2 @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['pypy-3.7', '3.7', 'pypy-3.8', '3.8', 'pypy-3.9', '3.9', '3.10', '3.11'] + python: ['pypy-3.8', '3.8', 'pypy-3.9', '3.9', 'pypy-3.10', '3.10', '3.11', '3.12'] check_formatting: ['0'] check_docs: ['0'] extra_name: [''] @@ -74,7 +74,7 @@ jobs: strategy: fail-fast: false matrix: - python: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: - name: Checkout uses: actions/checkout@v2 diff --git a/docs-requirements.in b/docs-requirements.in index 4acf894..f48700b 100644 --- a/docs-requirements.in +++ b/docs-requirements.in @@ -11,4 +11,4 @@ towncrier != 19.9.0,!= 21.3.0 # pytest-trio's own dependencies trio >= 0.22.0 outcome >= 1.1.0 -pytest >= 7.2.0 +pytest >= 8.2.0 diff --git a/docs-requirements.txt b/docs-requirements.txt index ae0c01d..efe32cb 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -1,6 +1,6 @@ # -# This file is autogenerated by pip-compile with python 3.8 -# To update, run: +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: # # pip-compile docs-requirements.in # @@ -12,7 +12,6 @@ attrs==22.1.0 # via # -r docs-requirements.in # outcome - # pytest # trio babel==2.10.3 # via sphinx @@ -40,8 +39,6 @@ idna==3.4 # trio imagesize==1.4.1 # via sphinx -importlib-metadata==5.0.0 - # via sphinx incremental==22.10.0 # via towncrier iniconfig==1.1.1 @@ -60,13 +57,13 @@ packaging==21.3 # via # pytest # sphinx -pluggy==1.0.0 +pluggy==1.5.0 # via pytest pygments==2.13.0 # via sphinx pyparsing==3.0.9 # via packaging -pytest==7.2.0 +pytest==8.2.2 # via -r docs-requirements.in pytz==2022.5 # via babel @@ -109,8 +106,6 @@ trio==0.22.0 # via -r docs-requirements.in urllib3==1.26.12 # via requests -zipp==3.10.0 - # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/setup.py b/setup.py index 9fbb81f..997b19a 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ packages=find_packages(), entry_points={"pytest11": ["trio = pytest_trio.plugin"]}, install_requires=[ - "trio >= 0.22.0", # for ExceptionGroup support + "trio >= 0.25.1", # for upstream Hypothesis integration "outcome >= 1.1.0", "pytest >= 7.2.0", # for ExceptionGroup support ], @@ -34,11 +34,11 @@ "Operating System :: MacOS :: MacOS X", "Operating System :: Microsoft :: Windows", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: System :: Networking", diff --git a/test-requirements.in b/test-requirements.in new file mode 100644 index 0000000..49db040 --- /dev/null +++ b/test-requirements.in @@ -0,0 +1,3 @@ +pytest>=8.2.2 +pytest-cov>=5.0.0 +hypothesis>=6.108.0 diff --git a/test-requirements.txt b/test-requirements.txt index b1d1509..6615483 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,34 @@ -pytest==7.2.0 -pytest-cov==4.0.0 -hypothesis==6.56.4 +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile test-requirements.in +# +attrs==23.2.0 + # via hypothesis +coverage[toml]==7.6.0 + # via pytest-cov +exceptiongroup==1.2.2 + # via + # hypothesis + # pytest +hypothesis==6.108.0 + # via -r test-requirements.in +iniconfig==2.0.0 + # via pytest +packaging==24.1 + # via pytest +pluggy==1.5.0 + # via pytest +pytest==8.2.2 + # via + # -r test-requirements.in + # pytest-cov +pytest-cov==5.0.0 + # via -r test-requirements.in +sortedcontainers==2.4.0 + # via hypothesis +tomli==2.0.1 + # via + # coverage + # pytest From 9b9801fdc9732bdb3c5005799b74624ed3a122fe Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Sun, 14 Jul 2024 13:41:23 -0700 Subject: [PATCH 183/185] update docs config --- .readthedocs.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index 2a32d6c..8ca3e92 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,12 +1,16 @@ # https://docs.readthedocs.io/en/latest/config-file/index.html version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3.9" + formats: - htmlzip - epub python: - version: 3.7 install: - requirements: docs-requirements.txt From 8164aef33d2dea001408c28ee999e1addb0ffc71 Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Tue, 30 Jul 2024 21:17:36 -0500 Subject: [PATCH 184/185] Move all configuration to pyproject.toml --- .coveragerc | 9 ---- pyproject.toml | 91 +++++++++++++++++++++++++++++++++++++++++ pytest_trio/_version.py | 2 +- setup.py | 50 ---------------------- 4 files changed, 92 insertions(+), 60 deletions(-) delete mode 100644 .coveragerc delete mode 100644 setup.py diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 5687b73..0000000 --- a/.coveragerc +++ /dev/null @@ -1,9 +0,0 @@ -[run] -branch=True -source=pytest_trio - -[report] -precision = 1 -exclude_lines = - pragma: no cover - abc.abstractmethod diff --git a/pyproject.toml b/pyproject.toml index 407fb8b..48270b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,67 @@ +[build-system] +requires = ["setuptools >= 64"] +build-backend = "setuptools.build_meta" + +[project] +name = "pytest-trio" +dynamic = ["version"] +authors = [ + { name="Emmanuel Leblond", email="emmanuel.leblond@gmail.com" }, +] +description = "Pytest plugin for trio" +readme = {file = "README.md", content-type = "text/markdown"} +license = {file = "LICENSE"} +requires-python = ">=3.8" +classifiers = [ + "Development Status :: 4 - Beta", + "License :: OSI Approved :: MIT License", + "License :: OSI Approved :: Apache Software License", + "Natural Language :: English", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Operating System :: OS Independent", + "Topic :: System :: Networking", + "Topic :: Software Development :: Testing", + "Framework :: Hypothesis", + "Framework :: Pytest", + "Framework :: Trio", +] +keywords = [ + "async", + "pytest", + "testing", + "trio", +] +dependencies = [ + "trio >= 0.25.1", # for upstream Hypothesis integration + "outcome >= 1.1.0", + "pytest >= 7.2.0", # for ExceptionGroup support +] + +[tool.setuptools.dynamic] +version = {attr = "pytest_trio._version.__version__"} + +[project.urls] +"Homepage" = "https://github.com/python-trio/pytest-trio" +"Source" = "https://github.com/python-trio/pytest-trio" +"Bug Tracker" = "https://github.com/python-trio/pytest-trio/issues" + +[project.entry-points.pytest11] +trio = "pytest_trio.plugin" + +[tool.setuptools.packages] +find = {namespaces = false} + [tool.towncrier] package = "pytest_trio" filename = "docs/source/history.rst" @@ -5,3 +69,30 @@ directory = "newsfragments" title_format = "pytest-trio {version} ({project_date})" underlines = ["-", "~", "^"] issue_format = "`#{issue} `__" + +[tool.coverage.run] +branch = true +source_pkgs = ["pytest_trio"] + +[tool.coverage.report] +precision = 1 +skip_covered = true +exclude_lines = [ + "pragma: no cover", + "abc.abstractmethod", + "if TYPE_CHECKING.*:", + "if _t.TYPE_CHECKING:", + "if t.TYPE_CHECKING:", + "@overload", + 'class .*\bProtocol\b.*\):', + "raise NotImplementedError", +] +partial_branches = [ + "pragma: no branch", + "if not TYPE_CHECKING:", + "if not _t.TYPE_CHECKING:", + "if not t.TYPE_CHECKING:", + "if .* or not TYPE_CHECKING:", + "if .* or not _t.TYPE_CHECKING:", + "if .* or not t.TYPE_CHECKING:", +] diff --git a/pytest_trio/_version.py b/pytest_trio/_version.py index a149830..2333e2d 100644 --- a/pytest_trio/_version.py +++ b/pytest_trio/_version.py @@ -1,3 +1,3 @@ -# This file is imported from __init__.py and exec'd from setup.py +# This file is imported from __init__.py and parsed by setuptools __version__ = "0.8.0+dev" diff --git a/setup.py b/setup.py deleted file mode 100644 index 997b19a..0000000 --- a/setup.py +++ /dev/null @@ -1,50 +0,0 @@ -from setuptools import setup, find_packages - -exec(open("pytest_trio/_version.py", encoding="utf-8").read()) - -LONG_DESC = open("README.rst", encoding="utf-8").read() - -setup( - name="pytest-trio", - version=__version__, - description="Pytest plugin for trio", - url="https://github.com/python-trio/pytest-trio", - long_description=open("README.rst").read(), - author="Emmanuel Leblond", - author_email="emmanuel.leblond@gmail.com", - license="MIT OR Apache-2.0", - packages=find_packages(), - entry_points={"pytest11": ["trio = pytest_trio.plugin"]}, - install_requires=[ - "trio >= 0.25.1", # for upstream Hypothesis integration - "outcome >= 1.1.0", - "pytest >= 7.2.0", # for ExceptionGroup support - ], - keywords=[ - "async", - "pytest", - "testing", - "trio", - ], - python_requires=">=3.7", - classifiers=[ - "License :: OSI Approved :: MIT License", - "License :: OSI Approved :: Apache Software License", - "Operating System :: POSIX :: Linux", - "Operating System :: MacOS :: MacOS X", - "Operating System :: Microsoft :: Windows", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - "Topic :: System :: Networking", - "Topic :: Software Development :: Testing", - "Framework :: Hypothesis", - "Framework :: Pytest", - "Framework :: Trio", - ], -) From 340d3fd3228d62e5550a45578a083e72cd0679cd Mon Sep 17 00:00:00 2001 From: CoolCat467 <52022020+CoolCat467@users.noreply.github.com> Date: Tue, 30 Jul 2024 21:22:45 -0500 Subject: [PATCH 185/185] Switch to using `build` --- ci.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ci.sh b/ci.sh index aae8b3c..0a6c4d6 100755 --- a/ci.sh +++ b/ci.sh @@ -23,11 +23,11 @@ function curl-harder() { } -python -m pip install -U pip setuptools wheel +python -m pip install -U pip build python -m pip --version -python setup.py sdist --formats=zip -python -m pip install dist/*.zip +python -m build +python -m pip install dist/*.whl if [ "$CHECK_FORMATTING" = "1" ]; then pip install black