Skip to content

Commit e426a59

Browse files
committed
[refactor] Attach package-scoped fixtures to a virtual/temporary Python module in the package, rather than attaching the fixture to the package's __init__.py.
Signed-off-by: Michael Seifert <[email protected]>
1 parent a88de75 commit e426a59

File tree

1 file changed

+63
-12
lines changed

1 file changed

+63
-12
lines changed

pytest_asyncio/plugin.py

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import sys
99
import warnings
1010
from asyncio import AbstractEventLoopPolicy
11+
from pathlib import Path
12+
from tempfile import NamedTemporaryFile
1113
from textwrap import dedent
1214
from typing import (
1315
Any,
@@ -625,18 +627,67 @@ def _patched_collect():
625627
elif type(collector) is Package:
626628

627629
def _patched_collect():
628-
# When collector is a package, collector.obj is the package's __init__.py.
629-
# pytest doesn't seem to collect fixtures in __init__.py.
630-
# Using parsefactories to collect fixtures in __init__.py their baseid will
631-
# end with "__init__.py", thus limiting the scope of the fixture to the
632-
# init module. Therefore, we tell the pluginmanager explicitly to collect
633-
# the fixtures in the init module, but strip "__init__.py" from the baseid
634-
# Possibly related to https://github.com/pytest-dev/pytest/issues/4085
635-
collector.obj.__pytest_asyncio_scoped_event_loop = scoped_event_loop
636-
fixturemanager = collector.config.pluginmanager.get_plugin("funcmanage")
637-
package_node_id = _removesuffix(collector.nodeid, "__init__.py")
638-
fixturemanager.parsefactories(collector.obj, nodeid=package_node_id)
639-
return collector.__original_collect()
630+
# When collector is a Package, collector.obj is the package's __init__.py.
631+
# Accessing the __init__.py to attach the fixture function may trigger
632+
# additional module imports or change the order of imports, which leads to
633+
# a number of problems.
634+
# see https://github.com/pytest-dev/pytest-asyncio/issues/729
635+
# Moreover, Package.obj has been removed in pytest 8.
636+
# Therefore, pytest-asyncio creates a temporary Python module inside the
637+
# collected package. The sole purpose of that module is to house a fixture
638+
# function for the pacakge-scoped event loop fixture. Once the fixture
639+
# has been evaluated by pytest, the temporary module can be removed.
640+
with NamedTemporaryFile(
641+
dir=collector.path.parent,
642+
prefix="pytest_asyncio_virtual_module_",
643+
suffix=".py",
644+
) as virtual_module_file:
645+
virtual_module = Module.from_parent(
646+
collector, path=Path(virtual_module_file.name)
647+
)
648+
virtual_module_file.write(
649+
dedent(
650+
f"""\
651+
import asyncio
652+
import pytest
653+
from pytest_asyncio.plugin import _temporary_event_loop_policy
654+
@pytest.fixture(
655+
scope="{collector_scope}",
656+
name="{collector.nodeid}::<event_loop>",
657+
)
658+
def scoped_event_loop(
659+
*args,
660+
event_loop_policy,
661+
):
662+
new_loop_policy = event_loop_policy
663+
with _temporary_event_loop_policy(new_loop_policy):
664+
loop = asyncio.new_event_loop()
665+
loop.__pytest_asyncio = True
666+
asyncio.set_event_loop(loop)
667+
yield loop
668+
loop.close()
669+
"""
670+
).encode()
671+
)
672+
virtual_module_file.flush()
673+
# Pytest's fixture matching algorithm compares a fixture's baseid with
674+
# an Item's nodeid to determine whether a fixture is available for a
675+
# specific Item. Since Package.nodeid ends with __init__.py, the
676+
# fixture's baseid will also end with __init__.py, which prevents
677+
# the fixture from being matched to test items in the current package.
678+
# Since the fixture matching is purely based on string comparison, we
679+
# strip the __init__.py suffix from the Package's node ID and
680+
# tell the fixturemanager to collect the fixture with the modified
681+
# nodeid. This makes the fixture visible to all items in the package.
682+
# see also https://github.com/pytest-dev/pytest/issues/11662#issuecomment-1879310072 # noqa
683+
# Possibly related to https://github.com/pytest-dev/pytest/issues/4085
684+
fixturemanager = collector.config.pluginmanager.get_plugin("funcmanage")
685+
package_node_id = _removesuffix(collector.nodeid, "__init__.py")
686+
fixturemanager.parsefactories(
687+
virtual_module.obj, nodeid=package_node_id
688+
)
689+
yield virtual_module
690+
yield from collector.__original_collect()
640691

641692
collector.__original_collect = collector.collect
642693
collector.collect = _patched_collect

0 commit comments

Comments
 (0)