Skip to content

Commit 10c8898

Browse files
authored
Merge pull request #11810 from pytest-dev/backport-11708-to-8.0.x
[8.0.x] FIX key formating divergence when inspecting plugin dictionary.
2 parents 3ae38ba + 5b7dded commit 10c8898

File tree

4 files changed

+102
-5
lines changed

4 files changed

+102
-5
lines changed

changelog/9765.bugfix.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fixed a frustrating bug that afflicted some users with the only error being ``assert mod not in mods``. The issue was caused by the fact that ``str(Path(mod))`` and ``mod.__file__`` don't necessarily produce the same string, and was being erroneously used interchangably in some places in the code.
2+
3+
This fix also broke the internal API of ``PytestPluginManager.consider_conftest`` by introducing a new parameter -- we mention this in case it is being used by external code, even if marked as *private*.

src/_pytest/config/__init__.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,8 @@ def _rget_with_confmod(
637637
def _importconftest(
638638
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
639639
) -> types.ModuleType:
640-
existing = self.get_plugin(str(conftestpath))
640+
conftestpath_plugin_name = str(conftestpath)
641+
existing = self.get_plugin(conftestpath_plugin_name)
641642
if existing is not None:
642643
return cast(types.ModuleType, existing)
643644

@@ -662,7 +663,7 @@ def _importconftest(
662663
assert mod not in mods
663664
mods.append(mod)
664665
self.trace(f"loading conftestmodule {mod!r}")
665-
self.consider_conftest(mod)
666+
self.consider_conftest(mod, registration_name=conftestpath_plugin_name)
666667
return mod
667668

668669
def _check_non_top_pytest_plugins(
@@ -742,9 +743,11 @@ def consider_pluginarg(self, arg: str) -> None:
742743
del self._name2plugin["pytest_" + name]
743744
self.import_plugin(arg, consider_entry_points=True)
744745

745-
def consider_conftest(self, conftestmodule: types.ModuleType) -> None:
746+
def consider_conftest(
747+
self, conftestmodule: types.ModuleType, registration_name: str
748+
) -> None:
746749
""":meta private:"""
747-
self.register(conftestmodule, name=conftestmodule.__file__)
750+
self.register(conftestmodule, name=registration_name)
748751

749752
def consider_env(self) -> None:
750753
""":meta private:"""

testing/acceptance_test.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import dataclasses
22
import importlib.metadata
33
import os
4+
import subprocess
45
import sys
56
import types
67

@@ -1390,3 +1391,61 @@ def test_boo(self):
13901391
)
13911392
result = pytester.runpytest_subprocess()
13921393
result.stdout.fnmatch_lines("*1 passed*")
1394+
1395+
1396+
@pytest.mark.skip(reason="Test is not isolated")
1397+
def test_issue_9765(pytester: Pytester) -> None:
1398+
"""Reproducer for issue #9765 on Windows
1399+
1400+
https://github.com/pytest-dev/pytest/issues/9765
1401+
"""
1402+
pytester.makepyprojecttoml(
1403+
"""
1404+
[tool.pytest.ini_options]
1405+
addopts = "-p my_package.plugin.my_plugin"
1406+
"""
1407+
)
1408+
pytester.makepyfile(
1409+
**{
1410+
"setup.py": (
1411+
"""
1412+
from setuptools import setup
1413+
1414+
if __name__ == '__main__':
1415+
setup(name='my_package', packages=['my_package', 'my_package.plugin'])
1416+
"""
1417+
),
1418+
"my_package/__init__.py": "",
1419+
"my_package/conftest.py": "",
1420+
"my_package/test_foo.py": "def test(): pass",
1421+
"my_package/plugin/__init__.py": "",
1422+
"my_package/plugin/my_plugin.py": (
1423+
"""
1424+
import pytest
1425+
1426+
def pytest_configure(config):
1427+
1428+
class SimplePlugin:
1429+
@pytest.fixture(params=[1, 2, 3])
1430+
def my_fixture(self, request):
1431+
yield request.param
1432+
1433+
config.pluginmanager.register(SimplePlugin())
1434+
"""
1435+
),
1436+
}
1437+
)
1438+
1439+
subprocess.run([sys.executable, "setup.py", "develop"], check=True)
1440+
try:
1441+
# We are using subprocess.run rather than pytester.run on purpose.
1442+
# pytester.run is adding the current directory to PYTHONPATH which avoids
1443+
# the bug. We also use pytest rather than python -m pytest for the same
1444+
# PYTHONPATH reason.
1445+
subprocess.run(
1446+
["pytest", "my_package"], capture_output=True, check=True, text=True
1447+
)
1448+
except subprocess.CalledProcessError as exc:
1449+
raise AssertionError(
1450+
f"pytest command failed:\n{exc.stdout=!s}\n{exc.stderr=!s}"
1451+
) from exc

testing/test_pluginmanager.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,38 @@ def pytest_configure(self):
9898
config.pluginmanager.register(A())
9999
assert len(values) == 2
100100

101+
@pytest.mark.skipif(
102+
not sys.platform.startswith("win"),
103+
reason="requires a case-insensitive file system",
104+
)
105+
def test_conftestpath_case_sensitivity(self, pytester: Pytester) -> None:
106+
"""Unit test for issue #9765."""
107+
config = pytester.parseconfig()
108+
pytester.makepyfile(**{"tests/conftest.py": ""})
109+
110+
conftest = pytester.path.joinpath("tests/conftest.py")
111+
conftest_upper_case = pytester.path.joinpath("TESTS/conftest.py")
112+
113+
mod = config.pluginmanager._importconftest(
114+
conftest,
115+
importmode="prepend",
116+
rootpath=pytester.path,
117+
)
118+
plugin = config.pluginmanager.get_plugin(str(conftest))
119+
assert plugin is mod
120+
121+
mod_uppercase = config.pluginmanager._importconftest(
122+
conftest_upper_case,
123+
importmode="prepend",
124+
rootpath=pytester.path,
125+
)
126+
plugin_uppercase = config.pluginmanager.get_plugin(str(conftest_upper_case))
127+
assert plugin_uppercase is mod_uppercase
128+
129+
# No str(conftestpath) normalization so conftest should be imported
130+
# twice and modules should be different objects
131+
assert mod is not mod_uppercase
132+
101133
def test_hook_tracing(self, _config_for_test: Config) -> None:
102134
pytestpm = _config_for_test.pluginmanager # fully initialized with plugins
103135
saveindent = []
@@ -368,7 +400,7 @@ def test_consider_conftest_deps(
368400
pytester.makepyfile("pytest_plugins='xyz'"), root=pytester.path
369401
)
370402
with pytest.raises(ImportError):
371-
pytestpm.consider_conftest(mod)
403+
pytestpm.consider_conftest(mod, registration_name="unused")
372404

373405

374406
class TestPytestPluginManagerBootstrapming:

0 commit comments

Comments
 (0)