Skip to content

Commit f1aa7a2

Browse files
authored
Merge pull request #9493 from bluetech/conftesting
Some conftest changes
2 parents 202e44b + 161bc48 commit f1aa7a2

File tree

5 files changed

+73
-52
lines changed

5 files changed

+73
-52
lines changed

changelog/9493.bugfix.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Symbolic link components are no longer resolved in conftest paths.
2+
This means that if a conftest appears twice in collection tree, using symlinks, it will be executed twice.
3+
For example, given
4+
5+
tests/real/conftest.py
6+
tests/real/test_it.py
7+
tests/link -> tests/real
8+
9+
running ``pytest tests`` now imports the conftest twice, once as ``tests/real/conftest.py`` and once as ``tests/link/conftest.py``.
10+
This is a fix to match a similar change made to test collection itself in pytest 6.0 (see :pull:`6523` for details).

src/_pytest/config/__init__.py

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Command line options, ini-file and conftest.py processing."""
22
import argparse
33
import collections.abc
4-
import contextlib
54
import copy
65
import enum
76
import inspect
@@ -345,14 +344,19 @@ def __init__(self) -> None:
345344
import _pytest.assertion
346345

347346
super().__init__("pytest")
348-
# The objects are module objects, only used generically.
349-
self._conftest_plugins: Set[types.ModuleType] = set()
350347

351-
# State related to local conftest plugins.
348+
# -- State related to local conftest plugins.
349+
# All loaded conftest modules.
350+
self._conftest_plugins: Set[types.ModuleType] = set()
351+
# All conftest modules applicable for a directory.
352+
# This includes the directory's own conftest modules as well
353+
# as those of its parent directories.
352354
self._dirpath2confmods: Dict[Path, List[types.ModuleType]] = {}
353-
self._conftestpath2mod: Dict[Path, types.ModuleType] = {}
355+
# Cutoff directory above which conftests are no longer discovered.
354356
self._confcutdir: Optional[Path] = None
357+
# If set, conftest loading is skipped.
355358
self._noconftest = False
359+
356360
self._duplicatepaths: Set[Path] = set()
357361

358362
# plugins that were explicitly skipped with pytest.skip
@@ -514,6 +518,19 @@ def _set_initial_conftests(
514518
if not foundanchor:
515519
self._try_load_conftest(current, namespace.importmode, rootpath)
516520

521+
def _is_in_confcutdir(self, path: Path) -> bool:
522+
"""Whether a path is within the confcutdir.
523+
524+
When false, should not load conftest.
525+
"""
526+
if self._confcutdir is None:
527+
return True
528+
try:
529+
path.relative_to(self._confcutdir)
530+
except ValueError:
531+
return False
532+
return True
533+
517534
def _try_load_conftest(
518535
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
519536
) -> None:
@@ -526,7 +543,7 @@ def _try_load_conftest(
526543

527544
def _getconftestmodules(
528545
self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
529-
) -> List[types.ModuleType]:
546+
) -> Sequence[types.ModuleType]:
530547
if self._noconftest:
531548
return []
532549

@@ -545,14 +562,12 @@ def _getconftestmodules(
545562
# and allow users to opt into looking into the rootdir parent
546563
# directories instead of requiring to specify confcutdir.
547564
clist = []
548-
confcutdir_parents = self._confcutdir.parents if self._confcutdir else []
549565
for parent in reversed((directory, *directory.parents)):
550-
if parent in confcutdir_parents:
551-
continue
552-
conftestpath = parent / "conftest.py"
553-
if conftestpath.is_file():
554-
mod = self._importconftest(conftestpath, importmode, rootpath)
555-
clist.append(mod)
566+
if self._is_in_confcutdir(parent):
567+
conftestpath = parent / "conftest.py"
568+
if conftestpath.is_file():
569+
mod = self._importconftest(conftestpath, importmode, rootpath)
570+
clist.append(mod)
556571
self._dirpath2confmods[directory] = clist
557572
return clist
558573

@@ -574,15 +589,9 @@ def _rget_with_confmod(
574589
def _importconftest(
575590
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
576591
) -> types.ModuleType:
577-
# Use a resolved Path object as key to avoid loading the same conftest
578-
# twice with build systems that create build directories containing
579-
# symlinks to actual files.
580-
# Using Path().resolve() is better than py.path.realpath because
581-
# it resolves to the correct path/drive in case-insensitive file systems (#5792)
582-
key = conftestpath.resolve()
583-
584-
with contextlib.suppress(KeyError):
585-
return self._conftestpath2mod[key]
592+
existing = self.get_plugin(str(conftestpath))
593+
if existing is not None:
594+
return cast(types.ModuleType, existing)
586595

587596
pkgpath = resolve_package_path(conftestpath)
588597
if pkgpath is None:
@@ -598,11 +607,10 @@ def _importconftest(
598607
self._check_non_top_pytest_plugins(mod, conftestpath)
599608

600609
self._conftest_plugins.add(mod)
601-
self._conftestpath2mod[key] = mod
602610
dirpath = conftestpath.parent
603611
if dirpath in self._dirpath2confmods:
604612
for path, mods in self._dirpath2confmods.items():
605-
if path and dirpath in path.parents or path == dirpath:
613+
if dirpath in path.parents or path == dirpath:
606614
assert mod not in mods
607615
mods.append(mod)
608616
self.trace(f"loading conftestmodule {mod!r}")

src/_pytest/main.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -689,9 +689,8 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]:
689689
# No point in finding packages when collecting doctests.
690690
if not self.config.getoption("doctestmodules", False):
691691
pm = self.config.pluginmanager
692-
confcutdir = pm._confcutdir
693692
for parent in (argpath, *argpath.parents):
694-
if confcutdir and parent in confcutdir.parents:
693+
if not pm._is_in_confcutdir(argpath):
695694
break
696695

697696
if parent.is_dir():

testing/test_conftest.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,9 @@ def test_issue151_load_all_conftests(pytester: Pytester) -> None:
146146
p = pytester.mkdir(name)
147147
p.joinpath("conftest.py").touch()
148148

149-
conftest = PytestPluginManager()
150-
conftest_setinitial(conftest, names)
151-
d = list(conftest._conftestpath2mod.values())
152-
assert len(d) == len(names)
149+
pm = PytestPluginManager()
150+
conftest_setinitial(pm, names)
151+
assert len(set(pm.get_plugins()) - {pm}) == len(names)
153152

154153

155154
def test_conftest_global_import(pytester: Pytester) -> None:
@@ -192,7 +191,7 @@ def test_conftestcutdir(pytester: Pytester) -> None:
192191
conf.parent, importmode="prepend", rootpath=pytester.path
193192
)
194193
assert len(values) == 0
195-
assert Path(conf) not in conftest._conftestpath2mod
194+
assert not conftest.has_plugin(str(conf))
196195
# but we can still import a conftest directly
197196
conftest._importconftest(conf, importmode="prepend", rootpath=pytester.path)
198197
values = conftest._getconftestmodules(
@@ -226,15 +225,15 @@ def test_setinitial_conftest_subdirs(pytester: Pytester, name: str) -> None:
226225
sub = pytester.mkdir(name)
227226
subconftest = sub.joinpath("conftest.py")
228227
subconftest.touch()
229-
conftest = PytestPluginManager()
230-
conftest_setinitial(conftest, [sub.parent], confcutdir=pytester.path)
228+
pm = PytestPluginManager()
229+
conftest_setinitial(pm, [sub.parent], confcutdir=pytester.path)
231230
key = subconftest.resolve()
232231
if name not in ("whatever", ".dotdir"):
233-
assert key in conftest._conftestpath2mod
234-
assert len(conftest._conftestpath2mod) == 1
232+
assert pm.has_plugin(str(key))
233+
assert len(set(pm.get_plugins()) - {pm}) == 1
235234
else:
236-
assert key not in conftest._conftestpath2mod
237-
assert len(conftest._conftestpath2mod) == 0
235+
assert not pm.has_plugin(str(key))
236+
assert len(set(pm.get_plugins()) - {pm}) == 0
238237

239238

240239
def test_conftest_confcutdir(pytester: Pytester) -> None:

testing/test_monkeypatch.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -50,21 +50,24 @@ class A:
5050

5151
class TestSetattrWithImportPath:
5252
def test_string_expression(self, monkeypatch: MonkeyPatch) -> None:
53-
monkeypatch.setattr("os.path.abspath", lambda x: "hello2")
54-
assert os.path.abspath("123") == "hello2"
53+
with monkeypatch.context() as mp:
54+
mp.setattr("os.path.abspath", lambda x: "hello2")
55+
assert os.path.abspath("123") == "hello2"
5556

5657
def test_string_expression_class(self, monkeypatch: MonkeyPatch) -> None:
57-
monkeypatch.setattr("_pytest.config.Config", 42)
58-
import _pytest
58+
with monkeypatch.context() as mp:
59+
mp.setattr("_pytest.config.Config", 42)
60+
import _pytest
5961

60-
assert _pytest.config.Config == 42 # type: ignore
62+
assert _pytest.config.Config == 42 # type: ignore
6163

6264
def test_unicode_string(self, monkeypatch: MonkeyPatch) -> None:
63-
monkeypatch.setattr("_pytest.config.Config", 42)
64-
import _pytest
65+
with monkeypatch.context() as mp:
66+
mp.setattr("_pytest.config.Config", 42)
67+
import _pytest
6568

66-
assert _pytest.config.Config == 42 # type: ignore
67-
monkeypatch.delattr("_pytest.config.Config")
69+
assert _pytest.config.Config == 42 # type: ignore
70+
mp.delattr("_pytest.config.Config")
6871

6972
def test_wrong_target(self, monkeypatch: MonkeyPatch) -> None:
7073
with pytest.raises(TypeError):
@@ -80,14 +83,16 @@ def test_unknown_attr(self, monkeypatch: MonkeyPatch) -> None:
8083

8184
def test_unknown_attr_non_raising(self, monkeypatch: MonkeyPatch) -> None:
8285
# https://github.com/pytest-dev/pytest/issues/746
83-
monkeypatch.setattr("os.path.qweqwe", 42, raising=False)
84-
assert os.path.qweqwe == 42 # type: ignore
86+
with monkeypatch.context() as mp:
87+
mp.setattr("os.path.qweqwe", 42, raising=False)
88+
assert os.path.qweqwe == 42 # type: ignore
8589

8690
def test_delattr(self, monkeypatch: MonkeyPatch) -> None:
87-
monkeypatch.delattr("os.path.abspath")
88-
assert not hasattr(os.path, "abspath")
89-
monkeypatch.undo()
90-
assert os.path.abspath
91+
with monkeypatch.context() as mp:
92+
mp.delattr("os.path.abspath")
93+
assert not hasattr(os.path, "abspath")
94+
mp.undo()
95+
assert os.path.abspath
9196

9297

9398
def test_delattr() -> None:

0 commit comments

Comments
 (0)