Skip to content

Commit 256ef7d

Browse files
committed
[fix] pytest-asyncio no longer uses virtual modules to install dynamic package-scoped fixtures.
The temporary files used for this mechanism appearing as disappear after they have been collected. This seems to create issues in some projects, such as setuptools. see #729 (comment) Signed-off-by: Michael Seifert <[email protected]>
1 parent 8ba9bd0 commit 256ef7d

File tree

1 file changed

+19
-95
lines changed

1 file changed

+19
-95
lines changed

pytest_asyncio/plugin.py

+19-95
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,10 @@
44
import enum
55
import functools
66
import inspect
7-
import os
87
import socket
98
import sys
109
import warnings
1110
from asyncio import AbstractEventLoopPolicy
12-
from pathlib import Path
13-
from tempfile import NamedTemporaryFile
1411
from textwrap import dedent
1512
from typing import (
1613
Any,
@@ -31,7 +28,6 @@
3128
)
3229

3330
import pytest
34-
from _pytest.pathlib import visit
3531
from pytest import (
3632
Class,
3733
Collector,
@@ -627,100 +623,28 @@ def _patched_collect():
627623
collector.__original_collect = collector.collect
628624
collector.collect = _patched_collect
629625
elif type(collector) is Package:
630-
if not collector.funcnamefilter(collector.name):
631-
return
632626

633627
def _patched_collect():
634-
# pytest.Package collects all files and sub-packages. Pytest 8 changes
635-
# this logic to only collect a single directory. Sub-packages are then
636-
# collected by a separate Package collector. Therefore, this logic can be
637-
# dropped, once we move to pytest 8.
638-
collector_dir = Path(collector.path.parent)
639-
for direntry in visit(str(collector_dir), recurse=collector._recurse):
640-
if not direntry.name == "__init__.py":
641-
# No need to register a package-scoped fixture, if we aren't
642-
# collecting a (sub-)package
643-
continue
644-
pkgdir = Path(direntry.path).parent
645-
pkg_nodeid = str(pkgdir.relative_to(collector_dir))
646-
if pkg_nodeid == ".":
647-
pkg_nodeid = ""
648-
# Pytest's fixture matching algorithm compares a fixture's baseid with
649-
# an Item's nodeid to determine whether a fixture is available for a
650-
# specific Item. Package.nodeid ends with __init__.py, so the
651-
# fixture's baseid will also end with __init__.py and prevents
652-
# the fixture from being matched to test items in the package.
653-
# Furthermore, Package also collects any sub-packages, which means
654-
# the ID of the scoped event loop for the package must change for
655-
# each sub-package.
656-
# As the fixture matching is purely based on string comparison, we
657-
# can assemble a path based on the root package path
658-
# (i.e. Package.path.parent) and the sub-package path
659-
# (i.e. Path(direntry.path).parent)). This makes the fixture visible
660-
# to all items in the package.
661-
# see also https://github.com/pytest-dev/pytest/issues/11662#issuecomment-1879310072 # noqa
662-
# Possibly related to https://github.com/pytest-dev/pytest/issues/4085
663-
fixture_id = f"{pkg_nodeid}/__init__.py::<event_loop>".lstrip("/")
664-
# When collector is a Package, collector.obj is the package's
665-
# __init__.py. Accessing the __init__.py to attach the fixture function
666-
# may trigger additional module imports or change the order of imports,
667-
# which leads to a number of problems.
668-
# see https://github.com/pytest-dev/pytest-asyncio/issues/729
669-
# Moreover, Package.obj has been removed in pytest 8.
670-
# Therefore, pytest-asyncio creates a temporary Python module inside the
671-
# collected package. The sole purpose of that module is to house a
672-
# fixture function for the pacakge-scoped event loop fixture. Once the
673-
# fixture has been evaluated by pytest, the temporary module
674-
# can be removed.
675-
with NamedTemporaryFile(
676-
dir=pkgdir,
677-
prefix="pytest_asyncio_virtual_module_",
678-
suffix=".py",
679-
delete=False, # Required for Windows compatibility
680-
) as virtual_module_file:
681-
virtual_module = Module.from_parent(
682-
collector, path=Path(virtual_module_file.name)
683-
)
684-
virtual_module_file.write(
685-
dedent(
686-
f"""\
687-
# This is a temporary file created by pytest-asyncio
688-
# If you see this file, a pytest run has crashed and
689-
# wasn't able to clean up the file in time.
690-
# You can safely remove this file.
691-
import asyncio
692-
import pytest
693-
from pytest_asyncio.plugin \
694-
import _temporary_event_loop_policy
695-
@pytest.fixture(
696-
scope="{collector_scope}",
697-
name="{fixture_id}",
698-
)
699-
def scoped_event_loop(
700-
*args,
701-
event_loop_policy,
702-
):
703-
new_loop_policy = event_loop_policy
704-
with _temporary_event_loop_policy(new_loop_policy):
705-
loop = asyncio.new_event_loop()
706-
loop.__pytest_asyncio = True
707-
asyncio.set_event_loop(loop)
708-
yield loop
709-
loop.close()
710-
"""
711-
).encode()
712-
)
713-
virtual_module_file.flush()
714-
fixturemanager = collector.config.pluginmanager.get_plugin(
715-
"funcmanage"
628+
# When collector is a Package, collector.obj is the package's
629+
# __init__.py. Accessing the __init__.py to attach the fixture function
630+
# may trigger additional module imports or change the order of imports,
631+
# which leads to a number of problems.
632+
# see https://github.com/pytest-dev/pytest-asyncio/issues/729
633+
# Moreover, Package.obj has been removed in pytest 8.
634+
# Therefore, pytest-asyncio attaches the packages-scoped event loop
635+
# fixture to the first collected module in that package.
636+
package_scoped_loop_added = False
637+
for subcollector in collector.__original_collect():
638+
if (
639+
not package_scoped_loop_added
640+
and isinstance(subcollector, Module)
641+
and getattr(subcollector, "obj", None)
642+
):
643+
subcollector.obj.__pytest_asyncio_package_scoped_event_loop = (
644+
scoped_event_loop
716645
)
717-
# Collect the fixtures in the virtual module with the node ID of
718-
# the current sub-package to ensure correct fixture matching.
719-
# see also https://github.com/pytest-dev/pytest/issues/11662#issuecomment-1879310072 # noqa
720-
fixturemanager.parsefactories(virtual_module.obj, nodeid=pkg_nodeid)
721-
yield virtual_module
722-
os.unlink(virtual_module_file.name)
723-
yield from collector.__original_collect()
646+
package_scoped_loop_added = True
647+
yield subcollector
724648

725649
collector.__original_collect = collector.collect
726650
collector.collect = _patched_collect

0 commit comments

Comments
 (0)