diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6aeaaa83..6f240e0a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -56,11 +56,12 @@ jobs: path: dist test: - name: Python ${{ matrix.python-version }} - runs-on: ubuntu-latest + name: ${{ matrix.os }} - Python ${{ matrix.python-version }} + runs-on: ${{ matrix.os }}-latest strategy: matrix: + os: [ubuntu, windows] python-version: ['3.8', '3.9', '3.10', '3.11', '3.12'] steps: @@ -75,7 +76,6 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - set -xe python -VV python -m site python -m pip install --upgrade pip @@ -85,6 +85,7 @@ jobs: - name: Store coverage data uses: actions/upload-artifact@v3 + if: "!endsWith(matrix.os, 'windows')" with: name: coverage-per-interpreter path: .coverage.* 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/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index 56be8787..73bb677a 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -1,11 +1,11 @@ -attrs==23.1.0 -coverage==7.3.4 +attrs==23.2.0 +coverage==7.4.0 exceptiongroup==1.2.0 -hypothesis==6.92.1 +hypothesis==6.92.5 iniconfig==2.0.0 packaging==23.2 pluggy==1.3.0 -pytest==7.4.3 +pytest==7.4.4 sortedcontainers==2.4.0 tomli==2.0.1 typing_extensions==4.9.0 diff --git a/dependencies/default/requirements.txt b/dependencies/default/requirements.txt index 0828607f..107f59f1 100644 --- a/dependencies/default/requirements.txt +++ b/dependencies/default/requirements.txt @@ -1,3 +1,3 @@ # Always adjust install_requires in setup.cfg and pytest-min-requirements.txt # when changing runtime dependencies -pytest >= 7.0.0 +pytest >= 7.0.0,<8 diff --git a/dependencies/docs/constraints.txt b/dependencies/docs/constraints.txt index c8938488..a8810c2d 100644 --- a/dependencies/docs/constraints.txt +++ b/dependencies/docs/constraints.txt @@ -1,4 +1,4 @@ -alabaster==0.7.13 +alabaster==0.7.15 Babel==2.14.0 certifi==2023.11.17 charset-normalizer==3.3.2 diff --git a/docs/source/reference/changelog.rst b/docs/source/reference/changelog.rst index b6f57af2..c0bdf5da 100644 --- a/docs/source/reference/changelog.rst +++ b/docs/source/reference/changelog.rst @@ -2,6 +2,16 @@ Changelog ========= +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 +------------ +As of v0.23, pytest-asyncio attaches an asyncio event loop to each item of the test suite (i.e. session, packages, modules, classes, functions) and allows tests to be run in those loops when marked accordingly. Pytest-asyncio currently assumes that async fixture scope is correlated with the new event loop scope. This prevents fixtures from being evaluated independently from the event loop scope and breaks some existing test suites (see `#706`_). For example, a test suite may require all fixtures and tests to run in the same event loop, but have async fixtures that are set up and torn down for each module. If you're affected by this issue, please continue using the v0.21 release, until it is resolved. + 0.23.3 (2024-01-01) =================== - Fixes a bug that caused event loops to be closed prematurely when using async generator fixtures with class scope or wider in a function-scoped test `#706 `_ 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 eb013f46..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): @@ -608,7 +611,13 @@ def scoped_event_loop( # know it exists. We work around this by attaching the fixture function to the # collected Python object, where it will be picked up by pytest.Class.collect() # or pytest.Module.collect(), respectively - if type(collector) is Module: + if type(collector) is Package: + # 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" # OutcomeException or a Collector.CollectError, if the "allow_module_level" @@ -617,28 +626,21 @@ def scoped_event_loop( # Therefore, we monkey patch Module.collect to add the scoped fixture to the # module before it runs the actual collection. def _patched_collect(): - collector.obj.__pytest_asyncio_scoped_event_loop = scoped_event_loop + # If the collected module is a DoctestTextfile, collector.obj is None + 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 collector.collect = _patched_collect - else: - pyobject = collector.obj - # If the collected module is a DoctestTextfile, collector.obj is None - if pyobject is None: - return - pyobject.__pytest_asyncio_scoped_event_loop = scoped_event_loop - # When collector is a package, collector.obj is the package's __init__.py. - # pytest doesn't seem to collect fixtures in __init__.py. - # Using parsefactories to collect fixtures in __init__.py their baseid will end - # with "__init__.py", thus limiting the scope of the fixture to the init module. - # Therefore, we tell the pluginmanager explicitly to collect the fixtures - # in the init module, but strip "__init__.py" from the baseid - # Possibly related to https://github.com/pytest-dev/pytest/issues/4085 - if isinstance(collector, Package): - fixturemanager = collector.config.pluginmanager.get_plugin("funcmanage") - package_node_id = _removesuffix(collector.nodeid, "__init__.py") - fixturemanager.parsefactories(collector.obj, nodeid=package_node_id) + elif isinstance(collector, Class): + collector.obj.__pytest_asyncio_scoped_event_loop = scoped_event_loop def _removesuffix(s: str, suffix: str) -> str: @@ -717,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/setup.cfg b/setup.cfg index fdbaf625..45d70b37 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,7 +40,7 @@ include_package_data = True # Always adjust requirements.txt and pytest-min-requirements.txt when changing runtime dependencies install_requires = - pytest >= 7.0.0 + pytest >= 7.0.0,<8 [options.extras_require] testing = 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/markers/test_package_scope.py b/tests/markers/test_package_scope.py index 1dc8a5c9..e0f44322 100644 --- a/tests/markers/test_package_scope.py +++ b/tests/markers/test_package_scope.py @@ -46,6 +46,7 @@ async def test_this_runs_in_same_loop(self): ), ) subpkg = pytester.mkpydir(subpackage_name) + subpkg.joinpath("__init__.py").touch() subpkg.joinpath("test_subpkg.py").write_text( dedent( f"""\ diff --git a/tests/test_import.py b/tests/test_import.py index 77352150..9912ae0c 100644 --- a/tests/test_import.py +++ b/tests/test_import.py @@ -3,7 +3,7 @@ from pytest import Pytester -def test_import_warning(pytester: Pytester): +def test_import_warning_does_not_cause_internal_error(pytester: Pytester): pytester.makepyfile( dedent( """\ @@ -16,3 +16,43 @@ async def test_errors_out(): ) result = pytester.runpytest("--asyncio-mode=auto") result.assert_outcomes(errors=1) + + +def test_import_warning_in_package_does_not_cause_internal_error(pytester: Pytester): + pytester.makepyfile( + __init__=dedent( + """\ + raise ImportWarning() + """ + ), + test_a=dedent( + """\ + async def test_errors_out(): + pass + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=auto") + result.assert_outcomes(errors=1) + + +def test_does_not_import_unrelated_packages(pytester: Pytester): + pkg_dir = pytester.mkpydir("mypkg") + pkg_dir.joinpath("__init__.py").write_text( + dedent( + """\ + raise ImportError() + """ + ), + ) + test_dir = pytester.mkdir("tests") + test_dir.joinpath("test_a.py").write_text( + dedent( + """\ + async def test_passes(): + pass + """ + ), + ) + result = pytester.runpytest("--asyncio-mode=auto") + result.assert_outcomes(passed=1) 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 14d3498a..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 @@ -8,7 +9,7 @@ # The default asyncio event loop implementation on Windows does not # support subprocesses. Subprocesses are available for Windows if a # ProactorEventLoop is used. - @pytest.yield_fixture() + @pytest.fixture() def event_loop(): loop = asyncio.ProactorEventLoop() yield loop