Merge pull request #9493 from bluetech/conftesting
Some conftest changes
This commit is contained in:
commit
f1aa7a25de
|
@ -0,0 +1,10 @@
|
||||||
|
Symbolic link components are no longer resolved in conftest paths.
|
||||||
|
This means that if a conftest appears twice in collection tree, using symlinks, it will be executed twice.
|
||||||
|
For example, given
|
||||||
|
|
||||||
|
tests/real/conftest.py
|
||||||
|
tests/real/test_it.py
|
||||||
|
tests/link -> tests/real
|
||||||
|
|
||||||
|
running ``pytest tests`` now imports the conftest twice, once as ``tests/real/conftest.py`` and once as ``tests/link/conftest.py``.
|
||||||
|
This is a fix to match a similar change made to test collection itself in pytest 6.0 (see :pull:`6523` for details).
|
|
@ -1,7 +1,6 @@
|
||||||
"""Command line options, ini-file and conftest.py processing."""
|
"""Command line options, ini-file and conftest.py processing."""
|
||||||
import argparse
|
import argparse
|
||||||
import collections.abc
|
import collections.abc
|
||||||
import contextlib
|
|
||||||
import copy
|
import copy
|
||||||
import enum
|
import enum
|
||||||
import inspect
|
import inspect
|
||||||
|
@ -345,14 +344,19 @@ class PytestPluginManager(PluginManager):
|
||||||
import _pytest.assertion
|
import _pytest.assertion
|
||||||
|
|
||||||
super().__init__("pytest")
|
super().__init__("pytest")
|
||||||
# The objects are module objects, only used generically.
|
|
||||||
self._conftest_plugins: Set[types.ModuleType] = set()
|
|
||||||
|
|
||||||
# State related to local conftest plugins.
|
# -- State related to local conftest plugins.
|
||||||
|
# All loaded conftest modules.
|
||||||
|
self._conftest_plugins: Set[types.ModuleType] = set()
|
||||||
|
# All conftest modules applicable for a directory.
|
||||||
|
# This includes the directory's own conftest modules as well
|
||||||
|
# as those of its parent directories.
|
||||||
self._dirpath2confmods: Dict[Path, List[types.ModuleType]] = {}
|
self._dirpath2confmods: Dict[Path, List[types.ModuleType]] = {}
|
||||||
self._conftestpath2mod: Dict[Path, types.ModuleType] = {}
|
# Cutoff directory above which conftests are no longer discovered.
|
||||||
self._confcutdir: Optional[Path] = None
|
self._confcutdir: Optional[Path] = None
|
||||||
|
# If set, conftest loading is skipped.
|
||||||
self._noconftest = False
|
self._noconftest = False
|
||||||
|
|
||||||
self._duplicatepaths: Set[Path] = set()
|
self._duplicatepaths: Set[Path] = set()
|
||||||
|
|
||||||
# plugins that were explicitly skipped with pytest.skip
|
# plugins that were explicitly skipped with pytest.skip
|
||||||
|
@ -514,6 +518,19 @@ class PytestPluginManager(PluginManager):
|
||||||
if not foundanchor:
|
if not foundanchor:
|
||||||
self._try_load_conftest(current, namespace.importmode, rootpath)
|
self._try_load_conftest(current, namespace.importmode, rootpath)
|
||||||
|
|
||||||
|
def _is_in_confcutdir(self, path: Path) -> bool:
|
||||||
|
"""Whether a path is within the confcutdir.
|
||||||
|
|
||||||
|
When false, should not load conftest.
|
||||||
|
"""
|
||||||
|
if self._confcutdir is None:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
path.relative_to(self._confcutdir)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def _try_load_conftest(
|
def _try_load_conftest(
|
||||||
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
|
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -526,7 +543,7 @@ class PytestPluginManager(PluginManager):
|
||||||
|
|
||||||
def _getconftestmodules(
|
def _getconftestmodules(
|
||||||
self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
|
self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||||
) -> List[types.ModuleType]:
|
) -> Sequence[types.ModuleType]:
|
||||||
if self._noconftest:
|
if self._noconftest:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
@ -545,14 +562,12 @@ class PytestPluginManager(PluginManager):
|
||||||
# and allow users to opt into looking into the rootdir parent
|
# and allow users to opt into looking into the rootdir parent
|
||||||
# directories instead of requiring to specify confcutdir.
|
# directories instead of requiring to specify confcutdir.
|
||||||
clist = []
|
clist = []
|
||||||
confcutdir_parents = self._confcutdir.parents if self._confcutdir else []
|
|
||||||
for parent in reversed((directory, *directory.parents)):
|
for parent in reversed((directory, *directory.parents)):
|
||||||
if parent in confcutdir_parents:
|
if self._is_in_confcutdir(parent):
|
||||||
continue
|
conftestpath = parent / "conftest.py"
|
||||||
conftestpath = parent / "conftest.py"
|
if conftestpath.is_file():
|
||||||
if conftestpath.is_file():
|
mod = self._importconftest(conftestpath, importmode, rootpath)
|
||||||
mod = self._importconftest(conftestpath, importmode, rootpath)
|
clist.append(mod)
|
||||||
clist.append(mod)
|
|
||||||
self._dirpath2confmods[directory] = clist
|
self._dirpath2confmods[directory] = clist
|
||||||
return clist
|
return clist
|
||||||
|
|
||||||
|
@ -574,15 +589,9 @@ class PytestPluginManager(PluginManager):
|
||||||
def _importconftest(
|
def _importconftest(
|
||||||
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
|
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||||
) -> types.ModuleType:
|
) -> types.ModuleType:
|
||||||
# Use a resolved Path object as key to avoid loading the same conftest
|
existing = self.get_plugin(str(conftestpath))
|
||||||
# twice with build systems that create build directories containing
|
if existing is not None:
|
||||||
# symlinks to actual files.
|
return cast(types.ModuleType, existing)
|
||||||
# Using Path().resolve() is better than py.path.realpath because
|
|
||||||
# it resolves to the correct path/drive in case-insensitive file systems (#5792)
|
|
||||||
key = conftestpath.resolve()
|
|
||||||
|
|
||||||
with contextlib.suppress(KeyError):
|
|
||||||
return self._conftestpath2mod[key]
|
|
||||||
|
|
||||||
pkgpath = resolve_package_path(conftestpath)
|
pkgpath = resolve_package_path(conftestpath)
|
||||||
if pkgpath is None:
|
if pkgpath is None:
|
||||||
|
@ -598,11 +607,10 @@ class PytestPluginManager(PluginManager):
|
||||||
self._check_non_top_pytest_plugins(mod, conftestpath)
|
self._check_non_top_pytest_plugins(mod, conftestpath)
|
||||||
|
|
||||||
self._conftest_plugins.add(mod)
|
self._conftest_plugins.add(mod)
|
||||||
self._conftestpath2mod[key] = mod
|
|
||||||
dirpath = conftestpath.parent
|
dirpath = conftestpath.parent
|
||||||
if dirpath in self._dirpath2confmods:
|
if dirpath in self._dirpath2confmods:
|
||||||
for path, mods in self._dirpath2confmods.items():
|
for path, mods in self._dirpath2confmods.items():
|
||||||
if path and dirpath in path.parents or path == dirpath:
|
if dirpath in path.parents or path == dirpath:
|
||||||
assert mod not in mods
|
assert mod not in mods
|
||||||
mods.append(mod)
|
mods.append(mod)
|
||||||
self.trace(f"loading conftestmodule {mod!r}")
|
self.trace(f"loading conftestmodule {mod!r}")
|
||||||
|
|
|
@ -689,9 +689,8 @@ class Session(nodes.FSCollector):
|
||||||
# No point in finding packages when collecting doctests.
|
# No point in finding packages when collecting doctests.
|
||||||
if not self.config.getoption("doctestmodules", False):
|
if not self.config.getoption("doctestmodules", False):
|
||||||
pm = self.config.pluginmanager
|
pm = self.config.pluginmanager
|
||||||
confcutdir = pm._confcutdir
|
|
||||||
for parent in (argpath, *argpath.parents):
|
for parent in (argpath, *argpath.parents):
|
||||||
if confcutdir and parent in confcutdir.parents:
|
if not pm._is_in_confcutdir(argpath):
|
||||||
break
|
break
|
||||||
|
|
||||||
if parent.is_dir():
|
if parent.is_dir():
|
||||||
|
|
|
@ -146,10 +146,9 @@ def test_issue151_load_all_conftests(pytester: Pytester) -> None:
|
||||||
p = pytester.mkdir(name)
|
p = pytester.mkdir(name)
|
||||||
p.joinpath("conftest.py").touch()
|
p.joinpath("conftest.py").touch()
|
||||||
|
|
||||||
conftest = PytestPluginManager()
|
pm = PytestPluginManager()
|
||||||
conftest_setinitial(conftest, names)
|
conftest_setinitial(pm, names)
|
||||||
d = list(conftest._conftestpath2mod.values())
|
assert len(set(pm.get_plugins()) - {pm}) == len(names)
|
||||||
assert len(d) == len(names)
|
|
||||||
|
|
||||||
|
|
||||||
def test_conftest_global_import(pytester: Pytester) -> None:
|
def test_conftest_global_import(pytester: Pytester) -> None:
|
||||||
|
@ -192,7 +191,7 @@ def test_conftestcutdir(pytester: Pytester) -> None:
|
||||||
conf.parent, importmode="prepend", rootpath=pytester.path
|
conf.parent, importmode="prepend", rootpath=pytester.path
|
||||||
)
|
)
|
||||||
assert len(values) == 0
|
assert len(values) == 0
|
||||||
assert Path(conf) not in conftest._conftestpath2mod
|
assert not conftest.has_plugin(str(conf))
|
||||||
# but we can still import a conftest directly
|
# but we can still import a conftest directly
|
||||||
conftest._importconftest(conf, importmode="prepend", rootpath=pytester.path)
|
conftest._importconftest(conf, importmode="prepend", rootpath=pytester.path)
|
||||||
values = conftest._getconftestmodules(
|
values = conftest._getconftestmodules(
|
||||||
|
@ -226,15 +225,15 @@ def test_setinitial_conftest_subdirs(pytester: Pytester, name: str) -> None:
|
||||||
sub = pytester.mkdir(name)
|
sub = pytester.mkdir(name)
|
||||||
subconftest = sub.joinpath("conftest.py")
|
subconftest = sub.joinpath("conftest.py")
|
||||||
subconftest.touch()
|
subconftest.touch()
|
||||||
conftest = PytestPluginManager()
|
pm = PytestPluginManager()
|
||||||
conftest_setinitial(conftest, [sub.parent], confcutdir=pytester.path)
|
conftest_setinitial(pm, [sub.parent], confcutdir=pytester.path)
|
||||||
key = subconftest.resolve()
|
key = subconftest.resolve()
|
||||||
if name not in ("whatever", ".dotdir"):
|
if name not in ("whatever", ".dotdir"):
|
||||||
assert key in conftest._conftestpath2mod
|
assert pm.has_plugin(str(key))
|
||||||
assert len(conftest._conftestpath2mod) == 1
|
assert len(set(pm.get_plugins()) - {pm}) == 1
|
||||||
else:
|
else:
|
||||||
assert key not in conftest._conftestpath2mod
|
assert not pm.has_plugin(str(key))
|
||||||
assert len(conftest._conftestpath2mod) == 0
|
assert len(set(pm.get_plugins()) - {pm}) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_conftest_confcutdir(pytester: Pytester) -> None:
|
def test_conftest_confcutdir(pytester: Pytester) -> None:
|
||||||
|
|
|
@ -50,21 +50,24 @@ def test_setattr() -> None:
|
||||||
|
|
||||||
class TestSetattrWithImportPath:
|
class TestSetattrWithImportPath:
|
||||||
def test_string_expression(self, monkeypatch: MonkeyPatch) -> None:
|
def test_string_expression(self, monkeypatch: MonkeyPatch) -> None:
|
||||||
monkeypatch.setattr("os.path.abspath", lambda x: "hello2")
|
with monkeypatch.context() as mp:
|
||||||
assert os.path.abspath("123") == "hello2"
|
mp.setattr("os.path.abspath", lambda x: "hello2")
|
||||||
|
assert os.path.abspath("123") == "hello2"
|
||||||
|
|
||||||
def test_string_expression_class(self, monkeypatch: MonkeyPatch) -> None:
|
def test_string_expression_class(self, monkeypatch: MonkeyPatch) -> None:
|
||||||
monkeypatch.setattr("_pytest.config.Config", 42)
|
with monkeypatch.context() as mp:
|
||||||
import _pytest
|
mp.setattr("_pytest.config.Config", 42)
|
||||||
|
import _pytest
|
||||||
|
|
||||||
assert _pytest.config.Config == 42 # type: ignore
|
assert _pytest.config.Config == 42 # type: ignore
|
||||||
|
|
||||||
def test_unicode_string(self, monkeypatch: MonkeyPatch) -> None:
|
def test_unicode_string(self, monkeypatch: MonkeyPatch) -> None:
|
||||||
monkeypatch.setattr("_pytest.config.Config", 42)
|
with monkeypatch.context() as mp:
|
||||||
import _pytest
|
mp.setattr("_pytest.config.Config", 42)
|
||||||
|
import _pytest
|
||||||
|
|
||||||
assert _pytest.config.Config == 42 # type: ignore
|
assert _pytest.config.Config == 42 # type: ignore
|
||||||
monkeypatch.delattr("_pytest.config.Config")
|
mp.delattr("_pytest.config.Config")
|
||||||
|
|
||||||
def test_wrong_target(self, monkeypatch: MonkeyPatch) -> None:
|
def test_wrong_target(self, monkeypatch: MonkeyPatch) -> None:
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
|
@ -80,14 +83,16 @@ class TestSetattrWithImportPath:
|
||||||
|
|
||||||
def test_unknown_attr_non_raising(self, monkeypatch: MonkeyPatch) -> None:
|
def test_unknown_attr_non_raising(self, monkeypatch: MonkeyPatch) -> None:
|
||||||
# https://github.com/pytest-dev/pytest/issues/746
|
# https://github.com/pytest-dev/pytest/issues/746
|
||||||
monkeypatch.setattr("os.path.qweqwe", 42, raising=False)
|
with monkeypatch.context() as mp:
|
||||||
assert os.path.qweqwe == 42 # type: ignore
|
mp.setattr("os.path.qweqwe", 42, raising=False)
|
||||||
|
assert os.path.qweqwe == 42 # type: ignore
|
||||||
|
|
||||||
def test_delattr(self, monkeypatch: MonkeyPatch) -> None:
|
def test_delattr(self, monkeypatch: MonkeyPatch) -> None:
|
||||||
monkeypatch.delattr("os.path.abspath")
|
with monkeypatch.context() as mp:
|
||||||
assert not hasattr(os.path, "abspath")
|
mp.delattr("os.path.abspath")
|
||||||
monkeypatch.undo()
|
assert not hasattr(os.path, "abspath")
|
||||||
assert os.path.abspath
|
mp.undo()
|
||||||
|
assert os.path.abspath
|
||||||
|
|
||||||
|
|
||||||
def test_delattr() -> None:
|
def test_delattr() -> None:
|
||||||
|
|
Loading…
Reference in New Issue