Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prevent collection issues with skip statements inside packages #764

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion docs/source/reference/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/pytest-dev/pytest-asyncio/issues/729>`_
- Addresses further issues that caused an internal pytest error during test collection
- Declares incompatibility with pytest 8 `#737 <https://github.com/pytest-dev/pytest-asyncio/issues/737>`_

Known issues
------------
Expand Down
1 change: 1 addition & 0 deletions pytest_asyncio/__init__.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
57 changes: 23 additions & 34 deletions pytest_asyncio/plugin.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""pytest-asyncio implementation."""

import asyncio
import contextlib
import enum
Expand Down Expand Up @@ -115,8 +116,7 @@ def fixture(
None,
] = ...,
name: Optional[str] = ...,
) -> FixtureFunction:
...
) -> FixtureFunction: ...


@overload
Expand All @@ -132,8 +132,7 @@ def fixture(
None,
] = ...,
name: Optional[str] = None,
) -> FixtureFunctionMarker:
...
) -> FixtureFunctionMarker: ...


def fixture(
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions tests/async_fixtures/test_async_fixtures_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
We support module-scoped async fixtures, but only if the event loop is
module-scoped too.
"""

import asyncio

import pytest
Expand Down
1 change: 1 addition & 0 deletions tests/hypothesis/test_base.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions tests/loop_fixture_scope/test_loop_fixture_scope.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Unit tests for overriding the event loop with a larger scoped one."""

import asyncio

import pytest
Expand Down
1 change: 1 addition & 0 deletions tests/markers/test_class_scope.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Test if pytestmark works when defined on a class."""

import asyncio
from textwrap import dedent

Expand Down
1 change: 1 addition & 0 deletions tests/test_simple.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Quick'n'dirty unit tests for provided fixtures and markers."""

import asyncio
from textwrap import dedent

Expand Down
30 changes: 30 additions & 0 deletions tests/test_skips.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
1 change: 1 addition & 0 deletions tests/test_subprocess.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for using subprocesses in tests."""

import asyncio.subprocess
import sys

Expand Down
Loading