diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4e5d2f8e..ed7d0cb0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: hooks: - id: yesqa - repo: https://github.com/Zac-HD/shed - rev: 0.10.7 + rev: 2024.1.1 hooks: - id: shed args: diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index 2fde5a76..c0bdf5da 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -2,10 +2,11 @@ Changelog ========= -0.23.4 (UNRELEASED) +0.23.4 (2024-01-28) =================== - pytest-asyncio no longer imports additional, unrelated packages during test collection `#729 `_ - Addresses further issues that caused an internal pytest error during test collection +- Declares incompatibility with pytest 8 `#737 `_ Known issues ------------ diff --git a/pytest_asyncio/__init__.py b/pytest_asyncio/__init__.py index 95046981..08dca478 100644 --- a/pytest_asyncio/__init__.py +++ b/pytest_asyncio/__init__.py @@ -1,4 +1,5 @@ """The main point for importing pytest-asyncio items.""" + from ._version import version as __version__ # noqa from .plugin import fixture, is_async_test diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 4729b267..b0744f0d 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -1,4 +1,5 @@ """pytest-asyncio implementation.""" + import asyncio import contextlib import enum @@ -115,8 +116,7 @@ def fixture( None, ] = ..., name: Optional[str] = ..., -) -> FixtureFunction: - ... +) -> FixtureFunction: ... @overload @@ -132,8 +132,7 @@ def fixture( None, ] = ..., name: Optional[str] = None, -) -> FixtureFunctionMarker: - ... +) -> FixtureFunctionMarker: ... def fixture( @@ -558,6 +557,10 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( Session: "session", } +# A stack used to push package-scoped loops during collection of a package +# and pop those loops during collection of a Module +__package_loop_stack: List[Union[FixtureFunctionMarker, FixtureFunction]] = [] + @pytest.hookimpl def pytest_collectstart(collector: pytest.Collector): @@ -609,31 +612,11 @@ def scoped_event_loop( # collected Python object, where it will be picked up by pytest.Class.collect() # or pytest.Module.collect(), respectively if type(collector) is Package: - - def _patched_collect(): - # When collector is a Package, collector.obj is the package's - # __init__.py. Accessing the __init__.py to attach the fixture function - # may trigger additional module imports or change the order of imports, - # which leads to a number of problems. - # see https://github.com/pytest-dev/pytest-asyncio/issues/729 - # Moreover, Package.obj has been removed in pytest 8. - # Therefore, pytest-asyncio attaches the packages-scoped event loop - # fixture to the first collected module in that package. - package_scoped_loop_added = False - for subcollector in collector.__original_collect(): - if ( - not package_scoped_loop_added - and isinstance(subcollector, Module) - and getattr(subcollector, "obj", None) - ): - subcollector.obj.__pytest_asyncio_package_scoped_event_loop = ( - scoped_event_loop - ) - package_scoped_loop_added = True - yield subcollector - - collector.__original_collect = collector.collect - collector.collect = _patched_collect + # Packages do not have a corresponding Python object. Therefore, the fixture + # for the package-scoped event loop is added to a stack. When a module inside + # the package is collected, the module will attach the fixture to its + # Python object. + __package_loop_stack.append(scoped_event_loop) elif isinstance(collector, Module): # Accessing Module.obj triggers a module import executing module-level # statements. A module-level pytest.skip statement raises the "Skipped" @@ -644,8 +627,14 @@ def _patched_collect(): # module before it runs the actual collection. def _patched_collect(): # If the collected module is a DoctestTextfile, collector.obj is None - if collector.obj is not None: - collector.obj.__pytest_asyncio_scoped_event_loop = scoped_event_loop + module = collector.obj + if module is not None: + module.__pytest_asyncio_scoped_event_loop = scoped_event_loop + try: + package_loop = __package_loop_stack.pop() + module.__pytest_asyncio_package_scoped_event_loop = package_loop + except IndexError: + pass return collector.__original_collect() collector.__original_collect = collector.collect @@ -730,9 +719,9 @@ def pytest_generate_tests(metafunc: Metafunc) -> None: # The fixture needs to be appended to avoid messing up the fixture evaluation # order metafunc.fixturenames.append(event_loop_fixture_id) - metafunc._arg2fixturedefs[ - event_loop_fixture_id - ] = fixturemanager._arg2fixturedefs[event_loop_fixture_id] + metafunc._arg2fixturedefs[event_loop_fixture_id] = ( + fixturemanager._arg2fixturedefs[event_loop_fixture_id] + ) @pytest.hookimpl(hookwrapper=True) diff --git a/tests/async_fixtures/test_async_fixtures_scope.py b/tests/async_fixtures/test_async_fixtures_scope.py index 079a981a..a25934a8 100644 --- a/tests/async_fixtures/test_async_fixtures_scope.py +++ b/tests/async_fixtures/test_async_fixtures_scope.py @@ -2,6 +2,7 @@ We support module-scoped async fixtures, but only if the event loop is module-scoped too. """ + import asyncio import pytest diff --git a/tests/hypothesis/test_base.py b/tests/hypothesis/test_base.py index c2a7ea6a..fa12f2b3 100644 --- a/tests/hypothesis/test_base.py +++ b/tests/hypothesis/test_base.py @@ -1,6 +1,7 @@ """Tests for the Hypothesis integration, which wraps async functions in a sync shim for Hypothesis. """ + from textwrap import dedent import pytest diff --git a/tests/loop_fixture_scope/test_loop_fixture_scope.py b/tests/loop_fixture_scope/test_loop_fixture_scope.py index 679ab48f..eb4be8c9 100644 --- a/tests/loop_fixture_scope/test_loop_fixture_scope.py +++ b/tests/loop_fixture_scope/test_loop_fixture_scope.py @@ -1,4 +1,5 @@ """Unit tests for overriding the event loop with a larger scoped one.""" + import asyncio import pytest diff --git a/tests/markers/test_class_scope.py b/tests/markers/test_class_scope.py index fa2fe81e..9ec3ed9c 100644 --- a/tests/markers/test_class_scope.py +++ b/tests/markers/test_class_scope.py @@ -1,4 +1,5 @@ """Test if pytestmark works when defined on a class.""" + import asyncio from textwrap import dedent diff --git a/tests/test_simple.py b/tests/test_simple.py index 05c92694..f5f52a8d 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -1,4 +1,5 @@ """Quick'n'dirty unit tests for provided fixtures and markers.""" + import asyncio from textwrap import dedent diff --git a/tests/test_skips.py b/tests/test_skips.py index abd9dd70..5d7aa303 100644 --- a/tests/test_skips.py +++ b/tests/test_skips.py @@ -105,3 +105,33 @@ async def test_is_skipped(): ) result = pytester.runpytest("--asyncio-mode=auto") result.assert_outcomes(skipped=1) + + +def test_skip_in_module_does_not_skip_package(pytester: Pytester): + pytester.makepyfile( + __init__="", + test_skip=dedent( + """\ + import pytest + + pytest.skip("Skip all tests", allow_module_level=True) + + def test_a(): + pass + + def test_b(): + pass + """ + ), + test_something=dedent( + """\ + import pytest + + @pytest.mark.asyncio + async def test_something(): + pass + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=strict") + result.assert_outcomes(passed=1, skipped=1) diff --git a/tests/test_subprocess.py b/tests/test_subprocess.py index f60b2824..3d91e7b1 100644 --- a/tests/test_subprocess.py +++ b/tests/test_subprocess.py @@ -1,4 +1,5 @@ """Tests for using subprocesses in tests.""" + import asyncio.subprocess import sys