diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e12ad385..89d990ac 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -58,20 +58,24 @@ jobs: test: name: ${{ matrix.os }} - Python ${{ matrix.python-version }} runs-on: ${{ matrix.os }}-latest - + continue-on-error: ${{ !matrix.required }} strategy: matrix: os: [ubuntu, windows] python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + required: [true] + include: + - os: ubuntu + python-version: 3.14-dev + required: false + - os: windows + python-version: 3.14-dev + required: false + steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 - if: "!endsWith(matrix.python-version, '-dev')" - with: - python-version: ${{ matrix.python-version }} - - uses: deadsnakes/action@v3.2.0 - if: endsWith(matrix.python-version, '-dev') with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -126,19 +130,51 @@ jobs: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} + prepare-release-notes: + name: Prepare Release Notes + needs: [lint] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install Python + uses: actions/setup-python@v5 + - name: Install towncrier + run: pip install towncrier==24.8.0 + - name: Install pandoc + run: | + sudo apt-get install -y pandoc + - name: Install pytest-asyncio + run: pip install . + - name: Compile Release Notes Draft + if: ${{ !contains(github.ref, 'refs/tags/') }} + run: towncrier build --draft --version "${{ needs.lint.outputs.version }}" > release-notes.rst + - name: Extract release notes from Git tag + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + run: | + set -e + git for-each-ref "${GITHUB_REF}" --format='%(contents)' > release-notes.rst + # Strip PGP signature from signed tags + sed -i "/-----BEGIN PGP SIGNATURE-----/,/-----END PGP SIGNATURE-----\n/d" release-notes.rst + - name: Convert Release Notes to Markdown + run: | + pandoc -s -o release-notes.md release-notes.rst + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: release-notes.md + path: release-notes.md + deploy: name: Deploy environment: release # Run only on pushing a tag if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') - needs: [lint, check] + needs: [lint, check, prepare-release-notes] runs-on: ubuntu-latest steps: - - name: Install pandoc - run: | - sudo apt-get install -y pandoc - - name: Checkout - uses: actions/checkout@v4 - name: Download distributions uses: actions/download-artifact@v4 with: @@ -147,20 +183,22 @@ jobs: - name: Collected dists run: | tree dist - - name: Convert README.rst to Markdown - run: | - pandoc -s -o README.md README.rst - name: PyPI upload uses: pypa/gh-action-pypi-publish@v1.12.4 with: attestations: true packages-dir: dist password: ${{ secrets.PYPI_API_TOKEN }} + - name: Download Release Notes + uses: actions/download-artifact@v4 + with: + name: release-notes.md + path: release-notes.md - name: GitHub Release uses: ncipollo/release-action@v1 with: name: pytest-asyncio ${{ needs.lint.outputs.version }} artifacts: dist/* - bodyFile: README.md + bodyFile: release-notes.md prerelease: ${{ needs.lint.outputs.prerelease }} token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 973ac3f5..bd705265 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: - id: check-merge-conflict exclude: rst$ - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.2 + rev: v0.11.10 hooks: - id: ruff args: [--fix] @@ -65,7 +65,7 @@ repos: - 'SC1004:' stages: [manual] - repo: https://github.com/sirosen/check-jsonschema - rev: 0.31.3 + rev: 0.33.0 hooks: - id: check-github-actions - repo: https://github.com/tox-dev/pyproject-fmt diff --git a/tests/loop_fixture_scope/__init__.py b/changelog.d/.gitkeep similarity index 100% rename from tests/loop_fixture_scope/__init__.py rename to changelog.d/.gitkeep diff --git a/dependencies/default/constraints.txt b/dependencies/default/constraints.txt index db8d6faf..d8cf2319 100644 --- a/dependencies/default/constraints.txt +++ b/dependencies/default/constraints.txt @@ -1,11 +1,11 @@ attrs==25.3.0 -coverage==7.7.1 -exceptiongroup==1.2.2 -hypothesis==6.130.3 +coverage==7.8.0 +exceptiongroup==1.3.0 +hypothesis==6.131.18 iniconfig==2.1.0 -packaging==24.2 -pluggy==1.5.0 +packaging==25.0 +pluggy==1.6.0 pytest==8.3.5 sortedcontainers==2.4.0 tomli==2.2.1 -typing_extensions==4.12.2 +typing_extensions==4.13.2 diff --git a/dependencies/docs/constraints.txt b/dependencies/docs/constraints.txt index 85afd831..06376f30 100644 --- a/dependencies/docs/constraints.txt +++ b/dependencies/docs/constraints.txt @@ -1,16 +1,16 @@ alabaster==0.7.16 Babel==2.17.0 -certifi==2025.1.31 -charset-normalizer==3.4.1 +certifi==2025.4.26 +charset-normalizer==3.4.2 docutils==0.21.2 idna==3.10 imagesize==1.4.1 Jinja2==3.1.6 MarkupSafe==3.0.2 -packaging==24.2 +packaging==25.0 Pygments==2.19.1 requests==2.32.3 -snowballstemmer==2.2.0 +snowballstemmer==3.0.1 Sphinx==8.0.2 sphinx-rtd-theme==3.0.2 sphinxcontrib-applehelp==2.0.0 @@ -20,4 +20,4 @@ sphinxcontrib-jquery==4.1 sphinxcontrib-jsmath==1.0.1 sphinxcontrib-qthelp==2.0.0 sphinxcontrib-serializinghtml==2.0.0 -urllib3==2.3.0 +urllib3==2.4.0 diff --git a/docs/how-to-guides/multiple_loops_example.py b/docs/how-to-guides/multiple_loops_example.py index a4c7a01c..2083e8b6 100644 --- a/docs/how-to-guides/multiple_loops_example.py +++ b/docs/how-to-guides/multiple_loops_example.py @@ -1,5 +1,9 @@ import asyncio -from asyncio import DefaultEventLoopPolicy +import warnings + +with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + from asyncio import DefaultEventLoopPolicy import pytest @@ -20,5 +24,6 @@ def event_loop_policy(request): @pytest.mark.asyncio +@pytest.mark.filterwarnings("ignore::DeprecationWarning") async def test_uses_custom_event_loop_policy(): assert isinstance(asyncio.get_event_loop_policy(), CustomEventLoopPolicy) diff --git a/docs/reference/changelog.rst b/docs/reference/changelog.rst index 11c35a1b..ea6ea8da 100644 --- a/docs/reference/changelog.rst +++ b/docs/reference/changelog.rst @@ -2,6 +2,50 @@ Changelog ========= +All notable changes to this project will be documented in this file. + +The format is based on `Keep a Changelog `__, and this project adheres to `Semantic Versioning `__. + +This project uses `towncrier `__ for changlog management and the changes for the upcoming release can be found in https://github.com/pytest-dev/pytest-asyncio/tree/main/changelog.d/. + +.. towncrier release notes start + +`1.0.0 `_ - 2025-05-26 +=============================================================================== + +Removed +------- + +- The deprecated *event_loop* fixture. (`#1106 `_) + + +Added +----- + +- Prelimiary support for Python 3.14 (`#1025 `_) + + +Changed +------- + +- Scoped event loops (e.g. module-scoped loops) are created once rather than per scope (e.g. per module). This reduces the number of fixtures and speeds up collection time, especially for large test suites. (`#1107 `_) +- The *loop_scope* argument to ``pytest.mark.asyncio`` no longer forces that a pytest Collector exists at the level of the specified scope. For example, a test function marked with ``pytest.mark.asyncio(loop_scope="class")`` no longer requires a class surrounding the test. This is consistent with the behavior of the *scope* argument to ``pytest_asyncio.fixture``. (`#1112 `_) + + +Fixed +----- + +- An error caused when using pytest's `--setup-plan` option. (`#630 `_) +- Unsuppressed import errors with pytest option ``--doctest-ignore-import-errors`` (`#797 `_) +- A "fixture not found" error in connection with package-scoped loops (`#1052 `_) + + +Notes for Downstream Packagers +------------------------------ + +- Removed a test that had an ordering dependency on other tests. (`#1114 `_) + + 0.26.0 (2025-03-25) =================== - Adds configuration option that sets default event loop scope for all tests `#793 `_ diff --git a/docs/reference/fixtures/event_loop_example.py b/docs/reference/fixtures/event_loop_example.py deleted file mode 100644 index b5a82b62..00000000 --- a/docs/reference/fixtures/event_loop_example.py +++ /dev/null @@ -1,5 +0,0 @@ -import asyncio - - -def test_event_loop_fixture(event_loop): - event_loop.run_until_complete(asyncio.sleep(0)) diff --git a/docs/reference/fixtures/event_loop_policy_example.py b/docs/reference/fixtures/event_loop_policy_example.py index 5fd87b73..e8642527 100644 --- a/docs/reference/fixtures/event_loop_policy_example.py +++ b/docs/reference/fixtures/event_loop_policy_example.py @@ -1,9 +1,14 @@ import asyncio +import warnings + +with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + from asyncio import DefaultEventLoopPolicy import pytest -class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): +class CustomEventLoopPolicy(DefaultEventLoopPolicy): pass @@ -13,5 +18,6 @@ def event_loop_policy(request): @pytest.mark.asyncio(loop_scope="module") +@pytest.mark.filterwarnings("ignore::DeprecationWarning") async def test_uses_custom_event_loop_policy(): assert isinstance(asyncio.get_event_loop_policy(), CustomEventLoopPolicy) diff --git a/docs/reference/fixtures/event_loop_policy_parametrized_example.py b/docs/reference/fixtures/event_loop_policy_parametrized_example.py index 1560889b..19552d81 100644 --- a/docs/reference/fixtures/event_loop_policy_parametrized_example.py +++ b/docs/reference/fixtures/event_loop_policy_parametrized_example.py @@ -1,10 +1,14 @@ import asyncio -from asyncio import DefaultEventLoopPolicy +import warnings + +with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + from asyncio import DefaultEventLoopPolicy import pytest -class CustomEventLoopPolicy(asyncio.DefaultEventLoopPolicy): +class CustomEventLoopPolicy(DefaultEventLoopPolicy): pass @@ -19,5 +23,6 @@ def event_loop_policy(request): @pytest.mark.asyncio +@pytest.mark.filterwarnings("ignore::DeprecationWarning") async def test_uses_custom_event_loop_policy(): assert isinstance(asyncio.get_event_loop_policy(), DefaultEventLoopPolicy) diff --git a/docs/reference/fixtures/index.rst b/docs/reference/fixtures/index.rst index 04953783..3d151dcb 100644 --- a/docs/reference/fixtures/index.rst +++ b/docs/reference/fixtures/index.rst @@ -2,33 +2,6 @@ Fixtures ======== -event_loop -========== -*This fixture is deprecated.* - -*If you want to request an asyncio event loop with a scope other than function -scope, use the "loop_scope" argument to* :ref:`reference/markers/asyncio` *when marking the tests. -If you want to return different types of event loops, use the* :ref:`reference/fixtures/event_loop_policy` -*fixture.* - -Creates a new asyncio event loop based on the current event loop policy. The new loop -is available as the return value of this fixture for synchronous functions, or via `asyncio.get_running_loop `__ for asynchronous functions. -The event loop is closed when the fixture scope ends. -The fixture scope defaults to ``function`` scope. - -.. include:: event_loop_example.py - :code: python - -Note that, when using the ``event_loop`` fixture, you need to interact with the event loop using methods like ``event_loop.run_until_complete``. If you want to *await* code inside your test function, you need to write a coroutine and use it as a test function. The :ref:`asyncio ` marker -is used to mark coroutines that should be treated as test functions. - -If you need to change the type of the event loop, prefer setting a custom event loop policy over redefining the ``event_loop`` fixture. - -If the ``pytest.mark.asyncio`` decorator is applied to a test function, the ``event_loop`` -fixture will be requested automatically by the test function. - -.. _reference/fixtures/event_loop_policy: - event_loop_policy ================= Returns the event loop policy used to create asyncio event loops. diff --git a/docs/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py b/docs/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py index afb4cc8a..5bb26247 100644 --- a/docs/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py +++ b/docs/reference/markers/class_scoped_loop_custom_policies_strict_mode_example.py @@ -1,12 +1,16 @@ -import asyncio +import warnings + +with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + from asyncio import DefaultEventLoopPolicy import pytest @pytest.fixture( params=[ - asyncio.DefaultEventLoopPolicy(), - asyncio.DefaultEventLoopPolicy(), + DefaultEventLoopPolicy(), + DefaultEventLoopPolicy(), ] ) def event_loop_policy(request): diff --git a/docs/reference/markers/index.rst b/docs/reference/markers/index.rst index e7d700c9..7715077b 100644 --- a/docs/reference/markers/index.rst +++ b/docs/reference/markers/index.rst @@ -21,21 +21,17 @@ The ``pytest.mark.asyncio`` marker can be omitted entirely in |auto mode|_ where By default, each test runs in it's own asyncio event loop. Multiple tests can share the same event loop by providing a *loop_scope* keyword argument to the *asyncio* mark. -The supported scopes are *class,* and *module,* and *package*. +The supported scopes are *function,* *class,* and *module,* *package,* and *session*. The following code example provides a shared event loop for all tests in `TestClassScopedLoop`: .. include:: class_scoped_loop_strict_mode_example.py :code: python -If you request class scope for a test that is not part of a class, it will result in a *UsageError*. Similar to class-scoped event loops, a module-scoped loop is provided when setting mark's scope to *module:* .. include:: module_scoped_loop_strict_mode_example.py :code: python -Package-scoped loops only work with `regular Python packages. `__ -That means they require an *__init__.py* to be present. -Package-scoped loops do not work in `namespace packages. `__ Subpackages do not share the loop with their parent package. Tests marked with *session* scope share the same event loop, even if the tests exist in different packages. diff --git a/pyproject.toml b/pyproject.toml index b368b481..b55bc8e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -121,7 +121,6 @@ asyncio_default_fixture_loop_scope = "function" junit_family = "xunit2" filterwarnings = [ "error", - "ignore:The event_loop fixture provided by pytest-asyncio has been redefined.*:DeprecationWarning", ] [tool.coverage.run] @@ -137,3 +136,44 @@ parallel = true [tool.coverage.report] show_missing = true + +[tool.towncrier] +directory = "changelog.d" +filename = "docs/reference/changelog.rst" +title_format = "`{version} `_ - {project_date}" +issue_format = "`#{issue} `_" + +[[tool.towncrier.type]] +directory = "security" +name = "Security" +showcontent = true + +[[tool.towncrier.type]] +directory = "removed" +name = "Removed" +showcontent = true + +[[tool.towncrier.type]] +directory = "deprecated" +name = "Deprecated" +showcontent = true + +[[tool.towncrier.type]] +directory = "added" +name = "Added" +showcontent = true + +[[tool.towncrier.type]] +directory = "changed" +name = "Changed" +showcontent = true + +[[tool.towncrier.type]] +directory = "fixed" +name = "Fixed" +showcontent = true + +[[tool.towncrier.type]] +directory = "downstream" +name = "Notes for Downstream Packagers" +showcontent = true diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py index 8a1d8733..aecf6e96 100644 --- a/pytest_asyncio/plugin.py +++ b/pytest_asyncio/plugin.py @@ -19,23 +19,22 @@ Generator, Iterable, Iterator, - Mapping, Sequence, ) -from textwrap import dedent from typing import ( Any, Callable, Literal, TypeVar, Union, + cast, overload, ) import pluggy import pytest +from _pytest.scope import Scope from pytest import ( - Class, Collector, Config, FixtureDef, @@ -44,14 +43,10 @@ Item, Mark, Metafunc, - Module, - Package, Parser, PytestCollectionWarning, PytestDeprecationWarning, PytestPluginManager, - Session, - StashKey, ) if sys.version_info >= (3, 10): @@ -71,10 +66,6 @@ class PytestAsyncioError(Exception): """Base class for exceptions raised by pytest-asyncio""" -class MultipleEventLoopsRequestedError(PytestAsyncioError): - """Raised when a test requests multiple asyncio event loops.""" - - class Mode(str, enum.Enum): AUTO = "auto" STRICT = "strict" @@ -259,24 +250,12 @@ def _preprocess_async_fixtures( # Ignore async fixtures without explicit asyncio mark in strict mode # This applies to pytest_trio fixtures, for example continue - scope = ( + loop_scope = ( getattr(func, "_loop_scope", None) or default_loop_scope or fixturedef.scope ) - if scope == "function" and "event_loop" not in fixturedef.argnames: - fixturedef.argnames += ("event_loop",) - _make_asyncio_fixture_function(func, scope) - function_signature = inspect.signature(func) - if "event_loop" in function_signature.parameters: - warnings.warn( - PytestDeprecationWarning( - f"{func.__name__} is asynchronous and explicitly " - f'requests the "event_loop" fixture. Asynchronous fixtures and ' - f'test functions should use "asyncio.get_running_loop()" ' - f"instead." - ) - ) + _make_asyncio_fixture_function(func, loop_scope) if "request" not in fixturedef.argnames: fixturedef.argnames += ("request",) _synchronize_async_fixture(fixturedef) @@ -295,15 +274,12 @@ def _synchronize_async_fixture(fixturedef: FixtureDef) -> None: def _add_kwargs( func: Callable[..., Any], kwargs: dict[str, Any], - event_loop: asyncio.AbstractEventLoop, request: FixtureRequest, ) -> dict[str, Any]: sig = inspect.signature(func) ret = kwargs.copy() if "request" in sig.parameters: ret["request"] = request - if "event_loop" in sig.parameters: - ret["event_loop"] = event_loop return ret @@ -334,7 +310,7 @@ def _asyncgen_fixture_wrapper(request: FixtureRequest, **kwargs: Any): ) event_loop = request.getfixturevalue(event_loop_fixture_id) kwargs.pop(event_loop_fixture_id, None) - gen_obj = func(**_add_kwargs(func, kwargs, event_loop, request)) + gen_obj = func(**_add_kwargs(func, kwargs, request)) async def setup(): res = await gen_obj.__anext__() # type: ignore[union-attr] @@ -383,7 +359,7 @@ def _async_fixture_wrapper(request: FixtureRequest, **kwargs: Any): kwargs.pop(event_loop_fixture_id, None) async def setup(): - res = await func(**_add_kwargs(func, kwargs, event_loop, request)) + res = await func(**_add_kwargs(func, kwargs, request)) return res context = contextvars.copy_context() @@ -410,21 +386,13 @@ async def setup(): def _get_event_loop_fixture_id_for_async_fixture( request: FixtureRequest, func: Any ) -> str: - default_loop_scope = request.config.getini("asyncio_default_fixture_loop_scope") + default_loop_scope = cast( + _ScopeName, request.config.getini("asyncio_default_fixture_loop_scope") + ) loop_scope = ( getattr(func, "_loop_scope", None) or default_loop_scope or request.scope ) - if loop_scope == "function": - event_loop_fixture_id = "event_loop" - else: - event_loop_node = _retrieve_scope_root(request._pyfuncitem, loop_scope) - event_loop_fixture_id = event_loop_node.stash.get( - # Type ignored because of non-optimal mypy inference. - _event_loop_fixture_id, # type: ignore[arg-type] - "", - ) - assert event_loop_fixture_id - return event_loop_fixture_id + return f"_{loop_scope}_event_loop" def _create_task_in_context( @@ -516,15 +484,6 @@ def _from_function(cls, function: Function, /) -> Function: ) subclass_instance.own_markers = function.own_markers assert subclass_instance.own_markers == function.own_markers - subclassed_function_signature = inspect.signature(subclass_instance.obj) - if "event_loop" in subclassed_function_signature.parameters: - subclass_instance.warn( - PytestDeprecationWarning( - f"{subclass_instance.name} is asynchronous and explicitly " - f'requests the "event_loop" fixture. Asynchronous fixtures and ' - f'test functions should use "asyncio.get_running_loop()" instead.' - ) - ) return subclass_instance @staticmethod @@ -671,151 +630,19 @@ def pytest_pycollect_makeitem_convert_async_functions_to_subclass( hook_result.force_result(updated_node_collection) -_event_loop_fixture_id = StashKey[str]() -_fixture_scope_by_collector_type: Mapping[type[pytest.Collector], _ScopeName] = { - Class: "class", - # Package is a subclass of module and the dict is used in isinstance checks - # Therefore, the order matters and Package needs to appear before Module - Package: "package", - Module: "module", - 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[Callable[..., Any]] = [] - - -@pytest.hookimpl -def pytest_collectstart(collector: pytest.Collector) -> None: - try: - collector_scope = next( - scope - for cls, scope in _fixture_scope_by_collector_type.items() - if isinstance(collector, cls) - ) - except StopIteration: - return - # Session is not a PyCollector type, so it doesn't have a corresponding - # "obj" attribute to attach a dynamic fixture function to. - # However, there's only one session per pytest run, so there's no need to - # create the fixture dynamically. We can simply define a session-scoped - # event loop fixture once in the plugin code. - if collector_scope == "session": - event_loop_fixture_id = _session_event_loop.__name__ - collector.stash[_event_loop_fixture_id] = event_loop_fixture_id - return - # There seem to be issues when a fixture is shadowed by another fixture - # and both differ in their params. - # https://github.com/pytest-dev/pytest/issues/2043 - # https://github.com/pytest-dev/pytest/issues/11350 - # As such, we assign a unique name for each event_loop fixture. - # The fixture name is stored in the collector's Stash, so it can - # be injected when setting up the test - event_loop_fixture_id = f"{collector.nodeid}::" - collector.stash[_event_loop_fixture_id] = event_loop_fixture_id - - @pytest.fixture( - scope=collector_scope, - name=event_loop_fixture_id, - ) - def scoped_event_loop( - *args, # Function needs to accept "cls" when collected by pytest.Class - event_loop_policy, - ) -> Iterator[asyncio.AbstractEventLoop]: - new_loop_policy = event_loop_policy - with ( - _temporary_event_loop_policy(new_loop_policy), - _provide_event_loop() as loop, - ): - asyncio.set_event_loop(loop) - yield loop - - # @pytest.fixture does not register the fixture anywhere, so pytest doesn't - # 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 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" - # kwargs is missing. These cases are handled correctly when they happen inside - # Collector.collect(), but this hook runs before the actual collect call. - # Therefore, we monkey patch Module.collect to add the scoped fixture to the - # module before it runs the actual collection. - def _patched_collect(): - # 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 # type: ignore[attr-defined] - collector.collect = _patched_collect # type: ignore[method-assign] - elif isinstance(collector, Class): - collector.obj.__pytest_asyncio_scoped_event_loop = scoped_event_loop - - @contextlib.contextmanager def _temporary_event_loop_policy(policy: AbstractEventLoopPolicy) -> Iterator[None]: - old_loop_policy = asyncio.get_event_loop_policy() + old_loop_policy = _get_event_loop_policy() try: old_loop = _get_event_loop_no_warn() except RuntimeError: old_loop = None - asyncio.set_event_loop_policy(policy) + _set_event_loop_policy(policy) try: yield finally: - # Try detecting user-created event loops that were left unclosed - # at the end of a test. - try: - current_loop: AbstractEventLoop | None = _get_event_loop_no_warn() - except RuntimeError: - current_loop = None - if current_loop is not None and not current_loop.is_closed(): - warnings.warn( - _UNCLOSED_EVENT_LOOP_WARNING % current_loop, - DeprecationWarning, - ) - current_loop.close() - - asyncio.set_event_loop_policy(old_loop_policy) - # When a test uses both a scoped event loop and the event_loop fixture, - # the "_provide_clean_event_loop" finalizer of the event_loop fixture - # will already have installed a fresh event loop, in order to shield - # subsequent tests from side-effects. We close this loop before restoring - # the old loop to avoid ResourceWarnings. - try: - _get_event_loop_no_warn().close() - except RuntimeError: - pass - asyncio.set_event_loop(old_loop) - - -_REDEFINED_EVENT_LOOP_FIXTURE_WARNING = dedent( - """\ - The event_loop fixture provided by pytest-asyncio has been redefined in - %s:%d - Replacing the event_loop fixture with a custom implementation is deprecated - and will lead to errors in the future. - If you want to request an asyncio event loop with a scope other than function - scope, use the "loop_scope" argument to the asyncio mark when marking the tests. - If you want to return different types of event loops, use the event_loop_policy - fixture. - """ -) + _set_event_loop_policy(old_loop_policy) + _set_event_loop(old_loop) @pytest.hookimpl(tryfirst=True) @@ -824,165 +651,36 @@ def pytest_generate_tests(metafunc: Metafunc) -> None: if not marker: return default_loop_scope = _get_default_test_loop_scope(metafunc.config) - scope = _get_marked_loop_scope(marker, default_loop_scope) - if scope == "function": + loop_scope = _get_marked_loop_scope(marker, default_loop_scope) + event_loop_fixture_id = f"_{loop_scope}_event_loop" + # This specific fixture name may already be in metafunc.argnames, if this + # test indirectly depends on the fixture. For example, this is the case + # when the test depends on an async fixture, both of which share the same + # event loop fixture mark. + if event_loop_fixture_id in metafunc.fixturenames: return - event_loop_node = _retrieve_scope_root(metafunc.definition, scope) - event_loop_fixture_id = event_loop_node.stash.get(_event_loop_fixture_id, None) - - if event_loop_fixture_id: - # This specific fixture name may already be in metafunc.argnames, if this - # test indirectly depends on the fixture. For example, this is the case - # when the test depends on an async fixture, both of which share the same - # event loop fixture mark. - if event_loop_fixture_id in metafunc.fixturenames: - return - fixturemanager = metafunc.config.pluginmanager.get_plugin("funcmanage") - assert fixturemanager is not None - if "event_loop" in metafunc.fixturenames: - raise MultipleEventLoopsRequestedError( - _MULTIPLE_LOOPS_REQUESTED_ERROR.format( - test_name=metafunc.definition.nodeid, - scope=scope, - scoped_loop_node=event_loop_node.nodeid, - ), - ) - # Add the scoped event loop fixture to Metafunc's list of fixture names and - # fixturedefs and leave the actual parametrization to pytest - # 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] - ) - - -@pytest.hookimpl(hookwrapper=True) -def pytest_fixture_setup( - fixturedef: FixtureDef, -) -> Generator[None, pluggy.Result, None]: - """Adjust the event loop policy when an event loop is produced.""" - if fixturedef.argname == "event_loop": - # The use of a fixture finalizer is preferred over the - # pytest_fixture_post_finalizer hook. The fixture finalizer is invoked once - # for each fixture, whereas the hook may be invoked multiple times for - # any specific fixture. - # see https://github.com/pytest-dev/pytest/issues/5848 - _add_finalizers( - fixturedef, - _close_event_loop, - _restore_event_loop_policy(asyncio.get_event_loop_policy()), - _provide_clean_event_loop, - ) - outcome = yield - loop: asyncio.AbstractEventLoop = outcome.get_result() - # Weird behavior was observed when checking for an attribute of FixtureDef.func - # Instead, we now check for a special attribute of the returned event loop - fixture_filename = inspect.getsourcefile(fixturedef.func) - if not _is_pytest_asyncio_loop(loop): - _, fixture_line_number = inspect.getsourcelines(fixturedef.func) - warnings.warn( - _REDEFINED_EVENT_LOOP_FIXTURE_WARNING - % (fixture_filename, fixture_line_number), - DeprecationWarning, - ) - policy = asyncio.get_event_loop_policy() - try: - old_loop = _get_event_loop_no_warn(policy) - if old_loop is not loop and not _is_pytest_asyncio_loop(old_loop): - old_loop.close() - except RuntimeError: - # Either the current event loop has been set to None - # or the loop policy doesn't specify to create new loops - # or we're not in the main thread - pass - policy.set_event_loop(loop) - return - - yield - - -def _make_pytest_asyncio_loop(loop: AbstractEventLoop) -> AbstractEventLoop: - loop.__pytest_asyncio = True # type: ignore[attr-defined] - return loop - - -def _is_pytest_asyncio_loop(loop: AbstractEventLoop) -> bool: - return getattr(loop, "__pytest_asyncio", False) - - -def _add_finalizers(fixturedef: FixtureDef, *finalizers: Callable[[], object]) -> None: - """ - Registers the specified fixture finalizers in the fixture. - - Finalizers need to be specified in the exact order in which they should be invoked. - - :param fixturedef: Fixture definition which finalizers should be added to - :param finalizers: Finalizers to be added - """ - for finalizer in reversed(finalizers): - fixturedef.addfinalizer(finalizer) - - -_UNCLOSED_EVENT_LOOP_WARNING = dedent( - """\ - pytest-asyncio detected an unclosed event loop when tearing down the event_loop - fixture: %r - pytest-asyncio will close the event loop for you, but future versions of the - library will no longer do so. In order to ensure compatibility with future - versions, please make sure that: - 1. Any custom "event_loop" fixture properly closes the loop after yielding it - 2. The scopes of your custom "event_loop" fixtures do not overlap - 3. Your code does not modify the event loop in async fixtures or tests - """ -) + fixturemanager = metafunc.config.pluginmanager.get_plugin("funcmanage") + assert fixturemanager is not None + # Add the scoped event loop fixture to Metafunc's list of fixture names and + # fixturedefs and leave the actual parametrization to pytest + # 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 + ] -def _close_event_loop() -> None: - policy = asyncio.get_event_loop_policy() - try: - loop = policy.get_event_loop() - except RuntimeError: - loop = None - if loop is not None and not _is_pytest_asyncio_loop(loop): - if not loop.is_closed(): - warnings.warn( - _UNCLOSED_EVENT_LOOP_WARNING % loop, - DeprecationWarning, - ) - loop.close() +def _get_event_loop_policy() -> AbstractEventLoopPolicy: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + return asyncio.get_event_loop_policy() -def _restore_event_loop_policy(previous_policy) -> Callable[[], None]: - def _restore_policy(): - # Close any event loop associated with the old loop policy - # to avoid ResourceWarnings in the _provide_clean_event_loop finalizer - try: - loop = _get_event_loop_no_warn(previous_policy) - except RuntimeError: - loop = None - if loop and not _is_pytest_asyncio_loop(loop): - loop.close() - asyncio.set_event_loop_policy(previous_policy) - - return _restore_policy - - -def _provide_clean_event_loop() -> None: - # At this point, the event loop for the current thread is closed. - # When a user calls asyncio.get_event_loop(), they will get a closed loop. - # In order to avoid this side effect from pytest-asyncio, we need to replace - # the current loop with a fresh one. - # Note that we cannot set the loop to None, because get_event_loop only creates - # a new loop, when set_event_loop has not been called. - policy = asyncio.get_event_loop_policy() - try: - old_loop = _get_event_loop_no_warn(policy) - except RuntimeError: - old_loop = None - if old_loop is not None and not _is_pytest_asyncio_loop(old_loop): - new_loop = policy.new_event_loop() - policy.set_event_loop(new_loop) +def _set_event_loop_policy(policy: AbstractEventLoopPolicy) -> None: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + asyncio.set_event_loop_policy(policy) def _get_event_loop_no_warn( @@ -996,6 +694,12 @@ def _get_event_loop_no_warn( return asyncio.get_event_loop() +def _set_event_loop(loop: AbstractEventLoop | None) -> None: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + asyncio.set_event_loop(loop) + + @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_pyfunc_call(pyfuncitem: Function) -> object | None: """ @@ -1079,29 +783,13 @@ def inner(*args, **kwargs): return inner -_MULTIPLE_LOOPS_REQUESTED_ERROR = dedent( - """\ - Multiple asyncio event loops with different scopes have been requested - by {test_name}. The test explicitly requests the event_loop fixture, while - another event loop with {scope} scope is provided by {scoped_loop_node}. - Remove "event_loop" from the requested fixture in your test to run the test - in a {scope}-scoped event loop or remove the scope argument from the "asyncio" - mark to run the test in a function-scoped event loop. - """ -) - - def pytest_runtest_setup(item: pytest.Item) -> None: marker = item.get_closest_marker("asyncio") if marker is None: return default_loop_scope = _get_default_test_loop_scope(item.config) - scope = _get_marked_loop_scope(marker, default_loop_scope) - if scope != "function": - parent_node = _retrieve_scope_root(item, scope) - event_loop_fixture_id = parent_node.stash[_event_loop_fixture_id] - else: - event_loop_fixture_id = "event_loop" + loop_scope = _get_marked_loop_scope(marker, default_loop_scope) + event_loop_fixture_id = f"_{loop_scope}_event_loop" fixturenames = item.fixturenames # type: ignore[attr-defined] if event_loop_fixture_id not in fixturenames: fixturenames.append(event_loop_fixture_id) @@ -1151,42 +839,36 @@ def _get_default_test_loop_scope(config: Config) -> _ScopeName: return config.getini("asyncio_default_test_loop_scope") -def _retrieve_scope_root(item: Collector | Item, scope: str) -> Collector: - node_type_by_scope = { - "class": Class, - "module": Module, - "package": Package, - "session": Session, - } - scope_root_type = node_type_by_scope[scope] - for node in reversed(item.listchain()): - if isinstance(node, scope_root_type): - assert isinstance(node, pytest.Collector) - return node - error_message = ( - f"{item.name} is marked to be run in an event loop with scope {scope}, " - f"but is not part of any {scope}." +def _create_scoped_event_loop_fixture(scope: _ScopeName) -> Callable: + @pytest.fixture( + scope=scope, + name=f"_{scope}_event_loop", ) - raise pytest.UsageError(error_message) + def _scoped_event_loop( + *args, # Function needs to accept "cls" when collected by pytest.Class + event_loop_policy, + ) -> Iterator[asyncio.AbstractEventLoop]: + new_loop_policy = event_loop_policy + with ( + _temporary_event_loop_policy(new_loop_policy), + _provide_event_loop() as loop, + ): + _set_event_loop(loop) + yield loop + return _scoped_event_loop -@pytest.fixture -def event_loop(request: FixtureRequest) -> Iterator[asyncio.AbstractEventLoop]: - """Create an instance of the default event loop for each test case.""" - new_loop_policy = request.getfixturevalue(event_loop_policy.__name__) - with _temporary_event_loop_policy(new_loop_policy), _provide_event_loop() as loop: - yield loop + +for scope in Scope: + globals()[f"_{scope.value}_event_loop"] = _create_scoped_event_loop_fixture( + scope.value + ) @contextlib.contextmanager def _provide_event_loop() -> Iterator[asyncio.AbstractEventLoop]: - loop = asyncio.get_event_loop_policy().new_event_loop() - # Add a magic value to the event loop, so pytest-asyncio can determine if the - # event_loop fixture was overridden. Other implementations of event_loop don't - # set this value. - # The magic value must be set as part of the function definition, because pytest - # seems to have multiple instances of the same FixtureDef or fixture function - loop = _make_pytest_asyncio_loop(loop) + policy = _get_event_loop_policy() + loop = policy.new_event_loop() try: yield loop finally: @@ -1200,20 +882,10 @@ def _provide_event_loop() -> Iterator[asyncio.AbstractEventLoop]: loop.close() -@pytest.fixture(scope="session") -def _session_event_loop( - request: FixtureRequest, event_loop_policy: AbstractEventLoopPolicy -) -> Iterator[asyncio.AbstractEventLoop]: - new_loop_policy = event_loop_policy - with _temporary_event_loop_policy(new_loop_policy), _provide_event_loop() as loop: - asyncio.set_event_loop(loop) - yield loop - - @pytest.fixture(scope="session", autouse=True) def event_loop_policy() -> AbstractEventLoopPolicy: """Return an instance of the policy used to create asyncio event loops.""" - return asyncio.get_event_loop_policy() + return _get_event_loop_policy() def is_async_test(item: Item) -> bool: diff --git a/tests/async_fixtures/test_async_fixtures_scope.py b/tests/async_fixtures/test_async_fixtures_scope.py deleted file mode 100644 index 7fbed781..00000000 --- a/tests/async_fixtures/test_async_fixtures_scope.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -We support module-scoped async fixtures, but only if the event loop is -module-scoped too. -""" - -from __future__ import annotations - -import asyncio - -import pytest - - -@pytest.fixture(scope="module") -def event_loop(): - """A module-scoped event loop.""" - loop = asyncio.new_event_loop() - yield loop - loop.close() - - -@pytest.fixture(scope="module") -async def async_fixture(): - await asyncio.sleep(0.1) - return 1 - - -@pytest.mark.asyncio -async def test_async_fixture_scope(async_fixture): - assert async_fixture == 1 - await asyncio.sleep(0.1) diff --git a/tests/async_fixtures/test_async_fixtures_with_finalizer.py b/tests/async_fixtures/test_async_fixtures_with_finalizer.py deleted file mode 100644 index 199ecbca..00000000 --- a/tests/async_fixtures/test_async_fixtures_with_finalizer.py +++ /dev/null @@ -1,63 +0,0 @@ -from __future__ import annotations - -import asyncio -import functools - -import pytest - -import pytest_asyncio - - -@pytest.mark.asyncio(loop_scope="module") -async def test_module_with_event_loop_finalizer(port_with_event_loop_finalizer): - await asyncio.sleep(0.01) - assert port_with_event_loop_finalizer - - -@pytest.mark.asyncio(loop_scope="module") -async def test_module_with_get_event_loop_finalizer(port_with_get_event_loop_finalizer): - await asyncio.sleep(0.01) - assert port_with_get_event_loop_finalizer - - -@pytest.fixture(scope="module") -def event_loop(): - """Change event_loop fixture to module level.""" - policy = asyncio.get_event_loop_policy() - loop = policy.new_event_loop() - yield loop - loop.close() - - -@pytest_asyncio.fixture(loop_scope="module", scope="module") -async def port_with_event_loop_finalizer(request): - def port_finalizer(finalizer): - async def port_afinalizer(): - # await task using loop provided by event_loop fixture - # RuntimeError is raised if task is created on a different loop - await finalizer - - asyncio.run(port_afinalizer()) - - worker = asyncio.ensure_future(asyncio.sleep(0.2)) - request.addfinalizer(functools.partial(port_finalizer, worker)) - return True - - -@pytest_asyncio.fixture(loop_scope="module", scope="module") -async def port_with_get_event_loop_finalizer(request): - def port_finalizer(finalizer): - async def port_afinalizer(): - # await task using current loop retrieved from the event loop policy - # RuntimeError is raised if task is created on a different loop. - # This can happen when pytest_fixture_setup - # does not set up the loop correctly, - # for example when policy.set_event_loop() is called with a wrong argument - await finalizer - - current_loop = asyncio.get_event_loop_policy().get_event_loop() - current_loop.run_until_complete(port_afinalizer()) - - worker = asyncio.ensure_future(asyncio.sleep(0.2)) - request.addfinalizer(functools.partial(port_finalizer, worker)) - return True diff --git a/tests/async_fixtures/test_async_gen_fixtures.py b/tests/async_fixtures/test_async_gen_fixtures.py deleted file mode 100644 index ddc2f5be..00000000 --- a/tests/async_fixtures/test_async_gen_fixtures.py +++ /dev/null @@ -1,53 +0,0 @@ -from __future__ import annotations - -import unittest.mock - -import pytest - -START = object() -END = object() -RETVAL = object() - - -@pytest.fixture(scope="module") -def mock(): - return unittest.mock.Mock(return_value=RETVAL) - - -@pytest.fixture -async def async_gen_fixture(mock): - try: - yield mock(START) - except Exception as e: - mock(e) - else: - mock(END) - - -@pytest.mark.asyncio -async def test_async_gen_fixture(async_gen_fixture, mock): - assert mock.called - assert mock.call_args_list[-1] == unittest.mock.call(START) - assert async_gen_fixture is RETVAL - - -@pytest.mark.asyncio -async def test_async_gen_fixture_finalized(mock): - try: - assert mock.called - assert mock.call_args_list[-1] == unittest.mock.call(END) - finally: - mock.reset_mock() - - -class TestAsyncGenFixtureMethod: - is_same_instance = False - - @pytest.fixture(autouse=True) - async def async_gen_fixture_method(self): - self.is_same_instance = True - yield None - - @pytest.mark.asyncio - async def test_async_gen_fixture_method(self): - assert self.is_same_instance diff --git a/tests/async_fixtures/test_parametrized_loop.py b/tests/async_fixtures/test_parametrized_loop.py deleted file mode 100644 index ca2cb5c7..00000000 --- a/tests/async_fixtures/test_parametrized_loop.py +++ /dev/null @@ -1,48 +0,0 @@ -from __future__ import annotations - -from textwrap import dedent - -from pytest import Pytester - - -def test_event_loop_parametrization(pytester: Pytester): - pytester.makepyfile( - dedent( - """\ - import asyncio - - import pytest - import pytest_asyncio - - TESTS_COUNT = 0 - - - def teardown_module(): - # parametrized 2 * 2 times: 2 for 'event_loop' and 2 for 'fix' - assert TESTS_COUNT == 4 - - - @pytest.fixture(scope="module", params=[1, 2]) - def event_loop(request): - request.param - loop = asyncio.new_event_loop() - yield loop - loop.close() - - - @pytest_asyncio.fixture(params=["a", "b"]) - async def fix(request): - await asyncio.sleep(0) - return request.param - - - @pytest.mark.asyncio - async def test_parametrized_loop(fix): - await asyncio.sleep(0) - global TESTS_COUNT - TESTS_COUNT += 1 - """ - ) - ) - result = pytester.runpytest_subprocess("--asyncio-mode=strict") - result.assert_outcomes(passed=4) diff --git a/tests/conftest.py b/tests/conftest.py index 76e2026f..eecab735 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,34 +1,3 @@ from __future__ import annotations -import asyncio - -import pytest - pytest_plugins = "pytester" - - -@pytest.fixture -def dependent_fixture(event_loop): - """A fixture dependent on the event_loop fixture, doing some cleanup.""" - counter = 0 - - async def just_a_sleep(): - """Just sleep a little while.""" - nonlocal event_loop - await asyncio.sleep(0.1) - nonlocal counter - counter += 1 - - event_loop.run_until_complete(just_a_sleep()) - yield - event_loop.run_until_complete(just_a_sleep()) - - assert counter == 2 - - -@pytest.fixture(scope="session", name="factory_involving_factories") -def factory_involving_factories_fixture(unused_tcp_port_factory): - def factory(): - return unused_tcp_port_factory() - - return factory diff --git a/tests/hypothesis/test_base.py b/tests/hypothesis/test_base.py index 4b185f62..487b05fe 100644 --- a/tests/hypothesis/test_base.py +++ b/tests/hypothesis/test_base.py @@ -45,43 +45,6 @@ async def test_mark_and_parametrize(x, y): assert y in (1, 2) -def test_can_use_explicit_event_loop_fixture(pytester: Pytester): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = module") - pytester.makepyfile( - dedent( - """\ - import asyncio - import pytest - from hypothesis import given - import hypothesis.strategies as st - - pytest_plugins = 'pytest_asyncio' - - @pytest.fixture(scope="module") - def event_loop(): - loop = asyncio.get_event_loop_policy().new_event_loop() - yield loop - loop.close() - - @given(st.integers()) - @pytest.mark.asyncio - async def test_explicit_fixture_request(event_loop, n): - semaphore = asyncio.Semaphore(value=0) - event_loop.call_soon(semaphore.release) - await semaphore.acquire() - """ - ) - ) - result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") - result.assert_outcomes(passed=1, warnings=2) - result.stdout.fnmatch_lines( - [ - '*is asynchronous and explicitly requests the "event_loop" fixture*', - "*event_loop fixture provided by pytest-asyncio has been redefined*", - ] - ) - - def test_async_auto_marked(pytester: Pytester): pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( diff --git a/tests/loop_fixture_scope/conftest.py b/tests/loop_fixture_scope/conftest.py deleted file mode 100644 index 4e8b06de..00000000 --- a/tests/loop_fixture_scope/conftest.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -import asyncio - -import pytest - - -class CustomSelectorLoop(asyncio.SelectorEventLoop): - """A subclass with no overrides, just to test for presence.""" - - -@pytest.fixture(scope="module") -def event_loop(): - """Create an instance of the default event loop for each test case.""" - loop = CustomSelectorLoop() - yield loop - loop.close() diff --git a/tests/loop_fixture_scope/test_loop_fixture_scope.py b/tests/loop_fixture_scope/test_loop_fixture_scope.py deleted file mode 100644 index eb1bae58..00000000 --- a/tests/loop_fixture_scope/test_loop_fixture_scope.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Unit tests for overriding the event loop with a larger scoped one.""" - -from __future__ import annotations - -import asyncio - -import pytest - - -@pytest.mark.asyncio -async def test_for_custom_loop(): - """This test should be executed using the custom loop.""" - await asyncio.sleep(0.01) - assert type(asyncio.get_event_loop()).__name__ == "CustomSelectorLoop" - - -@pytest.mark.asyncio -async def test_dependent_fixture(dependent_fixture): - await asyncio.sleep(0.1) diff --git a/tests/markers/test_class_scope.py b/tests/markers/test_class_scope.py index 4bddb4b8..e8732e86 100644 --- a/tests/markers/test_class_scope.py +++ b/tests/markers/test_class_scope.py @@ -82,29 +82,6 @@ async def test_this_runs_in_same_loop(self): result.assert_outcomes(passed=2) -def test_asyncio_mark_raises_when_class_scoped_is_request_without_class( - pytester: pytest.Pytester, -): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile( - dedent( - """\ - import asyncio - import pytest - - @pytest.mark.asyncio(loop_scope="class") - async def test_has_no_surrounding_class(): - pass - """ - ) - ) - result = pytester.runpytest("--asyncio-mode=strict") - result.assert_outcomes(errors=1) - result.stdout.fnmatch_lines( - "*is marked to be run in an event loop with scope*", - ) - - def test_asyncio_mark_is_inherited_to_subclasses(pytester: pytest.Pytester): pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( diff --git a/tests/markers/test_function_scope.py b/tests/markers/test_function_scope.py index c17a6225..f750ba58 100644 --- a/tests/markers/test_function_scope.py +++ b/tests/markers/test_function_scope.py @@ -92,29 +92,6 @@ async def test_warns(): result.stdout.fnmatch_lines("*DeprecationWarning*") -def test_function_scope_supports_explicit_event_loop_fixture_request( - pytester: Pytester, -): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile( - dedent( - """\ - import pytest - - pytestmark = pytest.mark.asyncio - - async def test_remember_loop(event_loop): - pass - """ - ) - ) - result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") - result.assert_outcomes(passed=1, warnings=1) - result.stdout.fnmatch_lines( - '*is asynchronous and explicitly requests the "event_loop" fixture*' - ) - - def test_asyncio_mark_respects_the_loop_policy( pytester: Pytester, ): diff --git a/tests/markers/test_module_scope.py b/tests/markers/test_module_scope.py index 7dbdbb7f..a050f503 100644 --- a/tests/markers/test_module_scope.py +++ b/tests/markers/test_module_scope.py @@ -5,59 +5,6 @@ from pytest import Pytester -def test_asyncio_mark_works_on_module_level(pytester: Pytester): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile( - dedent( - """\ - import asyncio - - import pytest - - pytestmark = pytest.mark.asyncio - - - class TestPyTestMark: - async def test_is_asyncio(self, event_loop, sample_fixture): - assert asyncio.get_event_loop() - - counter = 1 - - async def inc(): - nonlocal counter - counter += 1 - await asyncio.sleep(0) - - await asyncio.ensure_future(inc()) - assert counter == 2 - - - async def test_is_asyncio(event_loop, sample_fixture): - assert asyncio.get_event_loop() - counter = 1 - - async def inc(): - nonlocal counter - counter += 1 - await asyncio.sleep(0) - - await asyncio.ensure_future(inc()) - assert counter == 2 - - - @pytest.fixture - def sample_fixture(): - return None - """ - ) - ) - result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") - result.assert_outcomes(passed=2, warnings=2) - result.stdout.fnmatch_lines( - '*is asynchronous and explicitly requests the "event_loop" fixture*' - ) - - def test_asyncio_mark_provides_module_scoped_loop_strict_mode(pytester: Pytester): pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") pytester.makepyfile( @@ -89,28 +36,6 @@ async def test_this_runs_in_same_loop(self): result.assert_outcomes(passed=3) -def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop( - pytester: Pytester, -): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile( - dedent( - """\ - import asyncio - import pytest - - pytestmark = pytest.mark.asyncio(loop_scope="module") - - async def test_remember_loop(event_loop): - pass - """ - ) - ) - result = pytester.runpytest("--asyncio-mode=strict") - result.assert_outcomes(errors=1) - result.stdout.fnmatch_lines("*MultipleEventLoopsRequestedError: *") - - def test_asyncio_mark_respects_the_loop_policy( pytester: Pytester, ): diff --git a/tests/markers/test_package_scope.py b/tests/markers/test_package_scope.py index 204238a4..3e41459b 100644 --- a/tests/markers/test_package_scope.py +++ b/tests/markers/test_package_scope.py @@ -69,28 +69,6 @@ async def test_subpackage_runs_in_different_loop(): result.assert_outcomes(passed=4) -def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop( - pytester: Pytester, -): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile( - __init__="", - test_raises=dedent( - """\ - import asyncio - import pytest - - @pytest.mark.asyncio(loop_scope="package") - async def test_remember_loop(event_loop): - pass - """ - ), - ) - result = pytester.runpytest("--asyncio-mode=strict") - result.assert_outcomes(errors=1) - result.stdout.fnmatch_lines("*MultipleEventLoopsRequestedError: *") - - def test_asyncio_mark_respects_the_loop_policy( pytester: Pytester, ): @@ -361,23 +339,3 @@ async def test_does_not_fail(sets_event_loop_to_none, n): ) result = pytester.runpytest("--asyncio-mode=strict") result.assert_outcomes(passed=2) - - -def test_standalone_test_does_not_trigger_warning_about_no_current_event_loop_being_set( - pytester: Pytester, -): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile( - __init__="", - test_module=dedent( - """\ - import pytest - - @pytest.mark.asyncio(loop_scope="package") - async def test_anything(): - pass - """ - ), - ) - result = pytester.runpytest_subprocess("--asyncio-mode=strict") - result.assert_outcomes(warnings=0, passed=1) diff --git a/tests/markers/test_session_scope.py b/tests/markers/test_session_scope.py index 70e191b2..2d3a4993 100644 --- a/tests/markers/test_session_scope.py +++ b/tests/markers/test_session_scope.py @@ -70,28 +70,6 @@ async def test_subpackage_runs_in_same_loop(): result.assert_outcomes(passed=4) -def test_raise_when_event_loop_fixture_is_requested_in_addition_to_scoped_loop( - pytester: Pytester, -): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile( - __init__="", - test_raises=dedent( - """\ - import asyncio - import pytest - - @pytest.mark.asyncio(loop_scope="session") - async def test_remember_loop(event_loop): - pass - """ - ), - ) - result = pytester.runpytest("--asyncio-mode=strict") - result.assert_outcomes(errors=1) - result.stdout.fnmatch_lines("*MultipleEventLoopsRequestedError: *") - - def test_asyncio_mark_respects_the_loop_policy( pytester: Pytester, ): diff --git a/tests/test_dependent_fixtures.py b/tests/test_dependent_fixtures.py deleted file mode 100644 index 2e53700a..00000000 --- a/tests/test_dependent_fixtures.py +++ /dev/null @@ -1,16 +0,0 @@ -from __future__ import annotations - -import asyncio - -import pytest - - -@pytest.mark.asyncio -async def test_dependent_fixture(dependent_fixture): - """Test a dependent fixture.""" - await asyncio.sleep(0.1) - - -@pytest.mark.asyncio -async def test_factory_involving_factories(factory_involving_factories): - factory_involving_factories() diff --git a/tests/test_event_loop_fixture_finalizer.py b/tests/test_event_loop_fixture_finalizer.py deleted file mode 100644 index 1e378643..00000000 --- a/tests/test_event_loop_fixture_finalizer.py +++ /dev/null @@ -1,149 +0,0 @@ -from __future__ import annotations - -from textwrap import dedent - -from pytest import Pytester - - -def test_event_loop_fixture_finalizer_returns_fresh_loop_after_test(pytester: Pytester): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile( - dedent( - """\ - import asyncio - - import pytest - - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - @pytest.mark.asyncio - async def test_1(): - # This async test runs in its own event loop - global loop - running_loop = asyncio.get_event_loop_policy().get_event_loop() - # Make sure this test case received a different loop - assert running_loop is not loop - - def test_2(): - # Code outside of pytest-asyncio should not receive a "used" event loop - current_loop = asyncio.get_event_loop_policy().get_event_loop() - assert not current_loop.is_running() - assert not current_loop.is_closed() - """ - ) - ) - result = pytester.runpytest("--asyncio-mode=strict") - result.assert_outcomes(passed=2) - - -def test_event_loop_fixture_finalizer_handles_loop_set_to_none_sync( - pytester: Pytester, -): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile( - dedent( - """\ - import asyncio - - def test_sync(event_loop): - asyncio.get_event_loop_policy().set_event_loop(None) - """ - ) - ) - result = pytester.runpytest("--asyncio-mode=strict") - result.assert_outcomes(passed=1) - - -def test_event_loop_fixture_finalizer_handles_loop_set_to_none_async_without_fixture( - pytester: Pytester, -): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile( - dedent( - """\ - import asyncio - import pytest - - @pytest.mark.asyncio - async def test_async_without_explicit_fixture_request(): - asyncio.get_event_loop_policy().set_event_loop(None) - """ - ) - ) - result = pytester.runpytest("--asyncio-mode=strict") - result.assert_outcomes(passed=1) - - -def test_event_loop_fixture_finalizer_handles_loop_set_to_none_async_with_fixture( - pytester: Pytester, -): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile( - dedent( - """\ - import asyncio - import pytest - - @pytest.mark.asyncio - async def test_async_with_explicit_fixture_request(event_loop): - asyncio.get_event_loop_policy().set_event_loop(None) - """ - ) - ) - result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") - result.assert_outcomes(passed=1, warnings=1) - result.stdout.fnmatch_lines( - '*is asynchronous and explicitly requests the "event_loop" fixture*' - ) - - -def test_event_loop_fixture_finalizer_raises_warning_when_fixture_leaves_loop_unclosed( - pytester: Pytester, -): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile( - dedent( - """\ - import asyncio - import pytest - - pytest_plugins = 'pytest_asyncio' - - @pytest.fixture - def event_loop(): - loop = asyncio.get_event_loop_policy().new_event_loop() - yield loop - - @pytest.mark.asyncio - async def test_ends_with_unclosed_loop(): - pass - """ - ) - ) - result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "default") - result.assert_outcomes(passed=1, warnings=2) - result.stdout.fnmatch_lines("*unclosed event loop*") - - -def test_event_loop_fixture_finalizer_raises_warning_when_test_leaves_loop_unclosed( - pytester: Pytester, -): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile( - dedent( - """\ - import asyncio - import pytest - - pytest_plugins = 'pytest_asyncio' - - @pytest.mark.asyncio - async def test_ends_with_unclosed_loop(): - asyncio.set_event_loop(asyncio.new_event_loop()) - """ - ) - ) - result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W", "default") - result.assert_outcomes(passed=1, warnings=1) - result.stdout.fnmatch_lines("*unclosed event loop*") diff --git a/tests/test_event_loop_fixture_override_deprecation.py b/tests/test_event_loop_fixture_override_deprecation.py deleted file mode 100644 index 04859ef7..00000000 --- a/tests/test_event_loop_fixture_override_deprecation.py +++ /dev/null @@ -1,113 +0,0 @@ -from __future__ import annotations - -from textwrap import dedent - -from pytest import Pytester - - -def test_emit_warning_when_event_loop_fixture_is_redefined(pytester: Pytester): - pytester.makepyfile( - dedent( - """\ - import asyncio - import pytest - - @pytest.fixture - def event_loop(): - loop = asyncio.new_event_loop() - yield loop - loop.close() - - @pytest.mark.asyncio - async def test_emits_warning(): - pass - """ - ) - ) - result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") - result.assert_outcomes(passed=1, warnings=1) - result.stdout.fnmatch_lines( - ["*event_loop fixture provided by pytest-asyncio has been redefined*"] - ) - - -def test_emit_warning_when_event_loop_fixture_is_redefined_explicit_request( - pytester: Pytester, -): - pytester.makepyfile( - dedent( - """\ - import asyncio - import pytest - - @pytest.fixture - def event_loop(): - loop = asyncio.new_event_loop() - yield loop - loop.close() - - @pytest.mark.asyncio - async def test_emits_warning_when_requested_explicitly(event_loop): - pass - """ - ) - ) - result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") - result.assert_outcomes(passed=1, warnings=2) - result.stdout.fnmatch_lines( - ["*event_loop fixture provided by pytest-asyncio has been redefined*"] - ) - result.stdout.fnmatch_lines( - ['*is asynchronous and explicitly requests the "event_loop" fixture*'] - ) - - -def test_does_not_emit_warning_when_no_test_uses_the_event_loop_fixture( - pytester: Pytester, -): - pytester.makepyfile( - dedent( - """\ - import asyncio - import pytest - - @pytest.fixture - def event_loop(): - loop = asyncio.new_event_loop() - yield loop - loop.close() - - def test_emits_no_warning(): - pass - """ - ) - ) - result = pytester.runpytest_subprocess("--asyncio-mode=strict") - result.assert_outcomes(passed=1, warnings=0) - - -def test_emit_warning_when_redefined_event_loop_is_used_by_fixture(pytester: Pytester): - pytester.makepyfile( - dedent( - """\ - import asyncio - import pytest - import pytest_asyncio - - @pytest.fixture - def event_loop(): - loop = asyncio.new_event_loop() - yield loop - loop.close() - - @pytest_asyncio.fixture - async def uses_event_loop(): - pass - - def test_emits_warning(uses_event_loop): - pass - """ - ) - ) - result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") - result.assert_outcomes(passed=1, warnings=1) diff --git a/tests/test_explicit_event_loop_fixture_request.py b/tests/test_explicit_event_loop_fixture_request.py deleted file mode 100644 index c685ad84..00000000 --- a/tests/test_explicit_event_loop_fixture_request.py +++ /dev/null @@ -1,168 +0,0 @@ -from __future__ import annotations - -from textwrap import dedent - -from pytest import Pytester - - -def test_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine( - pytester: Pytester, -): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile( - dedent( - """\ - import pytest - - @pytest.mark.asyncio - async def test_coroutine_emits_warning(event_loop): - pass - """ - ) - ) - result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") - result.assert_outcomes(passed=1, warnings=1) - result.stdout.fnmatch_lines( - ['*is asynchronous and explicitly requests the "event_loop" fixture*'] - ) - - -def test_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine_method( - pytester: Pytester, -): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile( - dedent( - """\ - import pytest - - class TestEmitsWarning: - @pytest.mark.asyncio - async def test_coroutine_emits_warning(self, event_loop): - pass - """ - ) - ) - result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") - result.assert_outcomes(passed=1, warnings=1) - result.stdout.fnmatch_lines( - ['*is asynchronous and explicitly requests the "event_loop" fixture*'] - ) - - -def test_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine_staticmethod( - pytester: Pytester, -): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile( - dedent( - """\ - import pytest - - class TestEmitsWarning: - @staticmethod - @pytest.mark.asyncio - async def test_coroutine_emits_warning(event_loop): - pass - """ - ) - ) - result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") - result.assert_outcomes(passed=1, warnings=1) - result.stdout.fnmatch_lines( - ['*is asynchronous and explicitly requests the "event_loop" fixture*'] - ) - - -def test_emit_warning_when_event_loop_is_explicitly_requested_in_coroutine_fixture( - pytester: Pytester, -): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile( - dedent( - """\ - import pytest - import pytest_asyncio - - @pytest_asyncio.fixture - async def emits_warning(event_loop): - pass - - @pytest.mark.asyncio - async def test_uses_fixture(emits_warning): - pass - """ - ) - ) - result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") - result.assert_outcomes(passed=1, warnings=1) - result.stdout.fnmatch_lines( - ['*is asynchronous and explicitly requests the "event_loop" fixture*'] - ) - - -def test_emit_warning_when_event_loop_is_explicitly_requested_in_async_gen_fixture( - pytester: Pytester, -): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile( - dedent( - """\ - import pytest - import pytest_asyncio - - @pytest_asyncio.fixture - async def emits_warning(event_loop): - yield - - @pytest.mark.asyncio - async def test_uses_fixture(emits_warning): - pass - """ - ) - ) - result = pytester.runpytest_subprocess("--asyncio-mode=strict", "-W default") - result.assert_outcomes(passed=1, warnings=1) - result.stdout.fnmatch_lines( - ['*is asynchronous and explicitly requests the "event_loop" fixture*'] - ) - - -def test_does_not_emit_warning_when_event_loop_is_explicitly_requested_in_sync_function( - pytester: Pytester, -): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile( - dedent( - """\ - import pytest - - def test_uses_fixture(event_loop): - pass - """ - ) - ) - result = pytester.runpytest("--asyncio-mode=strict") - result.assert_outcomes(passed=1) - - -def test_does_not_emit_warning_when_event_loop_is_explicitly_requested_in_sync_fixture( - pytester: Pytester, -): - pytester.makeini("[pytest]\nasyncio_default_fixture_loop_scope = function") - pytester.makepyfile( - dedent( - """\ - import pytest - - @pytest.fixture - def any_fixture(event_loop): - pass - - def test_uses_fixture(any_fixture): - pass - """ - ) - ) - result = pytester.runpytest("--asyncio-mode=strict") - result.assert_outcomes(passed=1) diff --git a/tests/test_multiloop.py b/tests/test_multiloop.py deleted file mode 100644 index e6c852b9..00000000 --- a/tests/test_multiloop.py +++ /dev/null @@ -1,72 +0,0 @@ -from __future__ import annotations - -from textwrap import dedent - -from pytest import Pytester - - -def test_event_loop_override(pytester: Pytester): - pytester.makeconftest( - dedent( - '''\ - import asyncio - - import pytest - - - @pytest.fixture - def dependent_fixture(event_loop): - """A fixture dependent on the event_loop fixture, doing some cleanup.""" - counter = 0 - - async def just_a_sleep(): - """Just sleep a little while.""" - nonlocal event_loop - await asyncio.sleep(0.1) - nonlocal counter - counter += 1 - - event_loop.run_until_complete(just_a_sleep()) - yield - event_loop.run_until_complete(just_a_sleep()) - - assert counter == 2 - - - class CustomSelectorLoop(asyncio.SelectorEventLoop): - """A subclass with no overrides, just to test for presence.""" - - - @pytest.fixture - def event_loop(): - """Create an instance of the default event loop for each test case.""" - loop = CustomSelectorLoop() - yield loop - loop.close() - ''' - ) - ) - pytester.makepyfile( - dedent( - '''\ - """Unit tests for overriding the event loop.""" - import asyncio - - import pytest - - - @pytest.mark.asyncio - async def test_for_custom_loop(): - """This test should be executed using the custom loop.""" - await asyncio.sleep(0.01) - assert type(asyncio.get_event_loop()).__name__ == "CustomSelectorLoop" - - - @pytest.mark.asyncio - async def test_dependent_fixture(dependent_fixture): - await asyncio.sleep(0.1) - ''' - ) - ) - result = pytester.runpytest_subprocess("--asyncio-mode=strict") - result.assert_outcomes(passed=2, warnings=2) diff --git a/tests/test_simple.py b/tests/test_simple.py index b8a34fb2..f92ef4e7 100644 --- a/tests/test_simple.py +++ b/tests/test_simple.py @@ -14,13 +14,6 @@ async def async_coro(): return "ok" -def test_event_loop_fixture(event_loop): - """Test the injection of the event_loop fixture.""" - assert event_loop - ret = event_loop.run_until_complete(async_coro()) - assert ret == "ok" - - @pytest.mark.asyncio async def test_asyncio_marker(): """Test the asyncio pytest marker.""" diff --git a/tests/test_subprocess.py b/tests/test_subprocess.py index c32ba964..438f49f2 100644 --- a/tests/test_subprocess.py +++ b/tests/test_subprocess.py @@ -7,16 +7,6 @@ import pytest -if sys.platform == "win32": - # The default asyncio event loop implementation on Windows does not - # support subprocesses. Subprocesses are available for Windows if a - # ProactorEventLoop is used. - @pytest.fixture() - def event_loop(): - loop = asyncio.ProactorEventLoop() - yield loop - loop.close() - @pytest.mark.asyncio async def test_subprocess(): diff --git a/tox.ini b/tox.ini index 9a0cf93b..e6457f56 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 4.9.0 -envlist = py39, py310, py311, py312, py313, pytest-min, docs +envlist = py39, py310, py311, py312, py313, py314, pytest-min, docs isolated_build = true passenv = CI @@ -76,4 +76,5 @@ python = 3.11: py311 3.12: py312 3.13: py313 + 3.14-dev: py314 pypy3: pypy3