Skip to content
Prev Previous commit
Next Next commit
Construct full module names when using importmode=importlib
  • Loading branch information
nicoddemus committed Apr 2, 2021
commit f278286881087c41631d62bddb43ebf358e2f90f
29 changes: 18 additions & 11 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,9 @@ def pytest_configure(self, config: "Config") -> None:
#
# Internal API for local conftest plugin handling.
#
def _set_initial_conftests(self, namespace: argparse.Namespace) -> None:
def _set_initial_conftests(
self, namespace: argparse.Namespace, rootpath: Path
) -> None:
"""Load initial conftest files given a preparsed "namespace".

As conftest files may add their own command line options which have
Expand All @@ -507,15 +509,15 @@ def _set_initial_conftests(self, namespace: argparse.Namespace) -> None:
path = path[:i]
anchor = absolutepath(current / path)
if anchor.exists(): # we found some file object
self._try_load_conftest(anchor, namespace.importmode)
self._try_load_conftest(anchor, namespace.importmode, rootpath)
foundanchor = True
if not foundanchor:
self._try_load_conftest(current, namespace.importmode)
self._try_load_conftest(current, namespace.importmode, rootpath)

def _try_load_conftest(
self, anchor: Path, importmode: Union[str, ImportMode]
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
) -> None:
self._getconftestmodules(anchor, importmode)
self._getconftestmodules(anchor, importmode, rootpath)
# let's also consider test* subdirs
if anchor.is_dir():
for x in anchor.glob("test*"):
Expand All @@ -527,6 +529,7 @@ def _getconftestmodules(
self,
path: Path,
importmode: Union[str, ImportMode],
rootpath: Path
) -> List[types.ModuleType]:
if self._noconftest:
return []
Expand All @@ -545,7 +548,7 @@ def _getconftestmodules(
continue
conftestpath = parent / "conftest.py"
if conftestpath.is_file():
mod = self._importconftest(conftestpath, importmode)
mod = self._importconftest(conftestpath, importmode, rootpath)
clist.append(mod)
self._dirpath2confmods[directory] = clist
return clist
Expand All @@ -555,8 +558,9 @@ def _rget_with_confmod(
name: str,
path: Path,
importmode: Union[str, ImportMode],
rootpath: Path,
) -> Tuple[types.ModuleType, Any]:
modules = self._getconftestmodules(path, importmode)
modules = self._getconftestmodules(path, importmode, rootpath=rootpath)
for mod in reversed(modules):
try:
return mod, getattr(mod, name)
Expand All @@ -568,6 +572,7 @@ def _importconftest(
self,
conftestpath: Path,
importmode: Union[str, ImportMode],
rootpath: Path
) -> types.ModuleType:
# Use a resolved Path object as key to avoid loading the same conftest
# twice with build systems that create build directories containing
Expand All @@ -584,7 +589,7 @@ def _importconftest(
_ensure_removed_sysmodule(conftestpath.stem)

try:
mod = import_path(conftestpath, mode=importmode)
mod = import_path(conftestpath, mode=importmode, root=rootpath)
except Exception as e:
assert e.__traceback__ is not None
exc_info = (type(e), e, e.__traceback__)
Expand Down Expand Up @@ -1086,7 +1091,9 @@ def _processopt(self, opt: "Argument") -> None:

@hookimpl(trylast=True)
def pytest_load_initial_conftests(self, early_config: "Config") -> None:
self.pluginmanager._set_initial_conftests(early_config.known_args_namespace)
self.pluginmanager._set_initial_conftests(
early_config.known_args_namespace, rootpath=early_config.rootpath
)

def _initini(self, args: Sequence[str]) -> None:
ns, unknown_args = self._parser.parse_known_and_unknown_args(
Expand Down Expand Up @@ -1437,10 +1444,10 @@ def _getini(self, name: str):
assert type in [None, "string"]
return value

def _getconftest_pathlist(self, name: str, path: Path) -> Optional[List[Path]]:
def _getconftest_pathlist(self, name: str, path: Path, rootpath: Path) -> Optional[List[Path]]:
try:
mod, relroots = self.pluginmanager._rget_with_confmod(
name, path, self.getoption("importmode")
name, path, self.getoption("importmode"), rootpath
)
except KeyError:
return None
Expand Down
4 changes: 2 additions & 2 deletions src/_pytest/doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -534,11 +534,11 @@ def _find(

if self.path.name == "conftest.py":
module = self.config.pluginmanager._importconftest(
self.path, self.config.getoption("importmode")
self.path, self.config.getoption("importmode"), rootpath=self.config.rootpath
)
else:
try:
module = import_path(self.path)
module = import_path(self.path, root=self.config.rootpath)
except ImportError:
if self.config.getvalue("doctest_ignore_import_errors"):
pytest.skip("unable to import module %r" % self.path)
Expand Down
6 changes: 3 additions & 3 deletions src/_pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ def _in_venv(path: Path) -> bool:


def pytest_ignore_collect(fspath: Path, config: Config) -> Optional[bool]:
ignore_paths = config._getconftest_pathlist("collect_ignore", path=fspath.parent)
ignore_paths = config._getconftest_pathlist("collect_ignore", path=fspath.parent, rootpath=config.rootpath)
ignore_paths = ignore_paths or []
excludeopt = config.getoption("ignore")
if excludeopt:
Expand All @@ -388,7 +388,7 @@ def pytest_ignore_collect(fspath: Path, config: Config) -> Optional[bool]:
return True

ignore_globs = config._getconftest_pathlist(
"collect_ignore_glob", path=fspath.parent
"collect_ignore_glob", path=fspath.parent, rootpath=config.rootpath
)
ignore_globs = ignore_globs or []
excludeglobopt = config.getoption("ignore_glob")
Expand Down Expand Up @@ -546,7 +546,7 @@ def gethookproxy(self, fspath: "os.PathLike[str]"):
# hooks with all conftest.py files.
pm = self.config.pluginmanager
my_conftestmodules = pm._getconftestmodules(
Path(fspath), self.config.getoption("importmode")
Path(fspath), self.config.getoption("importmode"), rootpath=self.config.rootpath
)
remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
if remove_mods:
Expand Down
9 changes: 8 additions & 1 deletion src/_pytest/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,7 @@ def import_path(
p: Union[str, "os.PathLike[str]"],
*,
mode: Union[str, ImportMode] = ImportMode.prepend,
root: Path,
) -> ModuleType:
"""Import and return a module from the given path, which can be a file (a module) or
a directory (a package).
Expand All @@ -474,6 +475,11 @@ def import_path(
to import the module, which avoids having to use `__import__` and muck with `sys.path`
at all. It effectively allows having same-named test modules in different places.

:param root:
Is used when mode == ImportMode.importlib, and is used as an anchor to obtain
a unique module name for the module being imported, so it can safely be stashed
into ``sys.modules``.

:raises ImportPathMismatchError:
If after importing the given `path` and the module `__file__`
are different. Only raised in `prepend` and `append` modes.
Expand All @@ -486,7 +492,8 @@ def import_path(
raise ImportError(path)

if mode is ImportMode.importlib:
module_name = path.stem
relative_path = path.relative_to(root)
module_name = ".".join(relative_path.with_suffix("").parts)

for meta_importer in sys.meta_path:
spec = meta_importer.find_spec(module_name, [str(path.parent)])
Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@ def _importtestmodule(self):
# We assume we are only called once per module.
importmode = self.config.getoption("--import-mode")
try:
mod = import_path(self.path, mode=importmode)
mod = import_path(self.path, mode=importmode, root=self.config.rootpath)
except SyntaxError as e:
raise self.CollectError(
ExceptionInfo.from_current().getrepr(style="short")
Expand Down
6 changes: 3 additions & 3 deletions testing/python/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -2069,9 +2069,9 @@ def test_2(self):
reprec = pytester.inline_run("-v", "-s", "--confcutdir", pytester.path)
reprec.assertoutcome(passed=8)
config = reprec.getcalls("pytest_unconfigure")[0].config
values = config.pluginmanager._getconftestmodules(p, importmode="prepend")[
0
].values
values = config.pluginmanager._getconftestmodules(
p, importmode="prepend", rootpath=Path(testdir.tmpdir)
)[0].values
assert values == ["fin_a1", "fin_a2", "fin_b1", "fin_b2"] * 2

def test_scope_ordering(self, pytester: Pytester) -> None:
Expand Down
4 changes: 2 additions & 2 deletions testing/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -597,8 +597,8 @@ def test_getconftest_pathlist(self, pytester: Pytester, tmp_path: Path) -> None:
p = tmp_path.joinpath("conftest.py")
p.write_text(f"pathlist = ['.', {str(somepath)!r}]")
config = pytester.parseconfigure(p)
assert config._getconftest_pathlist("notexist", path=tmp_path) is None
pl = config._getconftest_pathlist("pathlist", path=tmp_path) or []
assert config._getconftest_pathlist("notexist", path=tmp_path, rootpath=tmp_path) is None
pl = config._getconftest_pathlist("pathlist", path=tmp_path, rootpath=tmp_path) or []
print(pl)
assert len(pl) == 2
assert pl[0] == tmp_path
Expand Down
57 changes: 39 additions & 18 deletions testing/test_conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def __init__(self) -> None:
self.importmode = "prepend"

namespace = cast(argparse.Namespace, Namespace())
conftest._set_initial_conftests(namespace)
conftest._set_initial_conftests(namespace, rootpath=Path(args[0]))


@pytest.mark.usefixtures("_sys_snapshot")
Expand All @@ -57,39 +57,45 @@ def basedir(
def test_basic_init(self, basedir: Path) -> None:
conftest = PytestPluginManager()
p = basedir / "adir"
assert conftest._rget_with_confmod("a", p, importmode="prepend")[1] == 1
assert conftest._rget_with_confmod("a", p, importmode="prepend", rootpath=basedir)[1] == 1

def test_immediate_initialiation_and_incremental_are_the_same(
self, basedir: Path
) -> None:
conftest = PytestPluginManager()
assert not len(conftest._dirpath2confmods)
conftest._getconftestmodules(basedir, importmode="prepend")
conftest._getconftestmodules(
basedir, importmode="prepend", rootpath=Path(basedir)
)
snap1 = len(conftest._dirpath2confmods)
assert snap1 == 1
conftest._getconftestmodules(basedir / "adir", importmode="prepend")
conftest._getconftestmodules(basedir / "adir", importmode="prepend", rootpath=basedir)
assert len(conftest._dirpath2confmods) == snap1 + 1
conftest._getconftestmodules(basedir / "b", importmode="prepend")
conftest._getconftestmodules(basedir / "b", importmode="prepend", rootpath=basedir)
assert len(conftest._dirpath2confmods) == snap1 + 2

def test_value_access_not_existing(self, basedir: Path) -> None:
conftest = ConftestWithSetinitial(basedir)
with pytest.raises(KeyError):
conftest._rget_with_confmod("a", basedir, importmode="prepend")
conftest._rget_with_confmod(
"a", basedir, importmode="prepend", rootpath=Path(basedir)
)

def test_value_access_by_path(self, basedir: Path) -> None:
conftest = ConftestWithSetinitial(basedir)
adir = basedir / "adir"
assert conftest._rget_with_confmod("a", adir, importmode="prepend")[1] == 1
assert conftest._rget_with_confmod("a", adir, importmode="prepend", rootpath=basedir)[1] == 1
assert (
conftest._rget_with_confmod("a", adir / "b", importmode="prepend")[1] == 1.5
conftest._rget_with_confmod("a", adir / "b", importmode="prepend", rootpath=basedir)[1] == 1.5
)

def test_value_access_with_confmod(self, basedir: Path) -> None:
startdir = basedir / "adir" / "b"
startdir.joinpath("xx").mkdir()
conftest = ConftestWithSetinitial(startdir)
mod, value = conftest._rget_with_confmod("a", startdir, importmode="prepend")
mod, value = conftest._rget_with_confmod(
"a", startdir, importmode="prepend", rootpath=Path(basedir)
)
assert value == 1.5
path = Path(mod.__file__)
assert path.parent == basedir / "adir" / "b"
Expand All @@ -110,7 +116,7 @@ def test_doubledash_considered(pytester: Pytester) -> None:
conf.joinpath("conftest.py").touch()
conftest = PytestPluginManager()
conftest_setinitial(conftest, [conf.name, conf.name])
values = conftest._getconftestmodules(conf, importmode="prepend")
values = conftest._getconftestmodules(conf, importmode="prepend", rootpath=pytester.path)
assert len(values) == 1


Expand All @@ -131,6 +137,7 @@ def test_conftest_global_import(pytester: Pytester) -> None:
p = pytester.makepyfile(
"""
from pathlib import Path
<<<<<<< HEAD
import pytest
from _pytest.config import PytestPluginManager
conf = PytestPluginManager()
Expand All @@ -143,6 +150,18 @@ def test_conftest_global_import(pytester: Pytester) -> None:
subconf = sub / "conftest.py"
subconf.write_text("y=4")
mod2 = conf._importconftest(subconf, importmode="prepend")
=======
import py, pytest
from _pytest.config import PytestPluginManager
conf = PytestPluginManager()
mod = conf._importconftest(py.path.local("conftest.py"), importmode="prepend", rootpath=Path.cwd)
assert mod.x == 3
import conftest
assert conftest is mod, (conftest, mod)
subconf = py.path.local().ensure("sub", "conftest.py")
subconf.write("y=4")
mod2 = conf._importconftest(subconf, importmode="prepend", rootpath=Path.cwd)
>>>>>>> 92960c5b3... Construct full module names when using importmode=importlib
assert mod != mod2
assert mod2.y == 4
import conftest
Expand All @@ -158,17 +177,17 @@ def test_conftestcutdir(pytester: Pytester) -> None:
p = pytester.mkdir("x")
conftest = PytestPluginManager()
conftest_setinitial(conftest, [pytester.path], confcutdir=p)
values = conftest._getconftestmodules(p, importmode="prepend")
values = conftest._getconftestmodules(p, importmode="prepend", rootpath=pytester.path)
assert len(values) == 0
values = conftest._getconftestmodules(conf.parent, importmode="prepend")
values = conftest._getconftestmodules(conf.parent, importmode="prepend", rootpath=pytester.path)
assert len(values) == 0
assert Path(conf) not in conftest._conftestpath2mod
# but we can still import a conftest directly
conftest._importconftest(conf, importmode="prepend")
values = conftest._getconftestmodules(conf.parent, importmode="prepend")
conftest._importconftest(conf, importmode="prepend", rootpath=pytester.path)
values = conftest._getconftestmodules(conf.parent, importmode="prepend", rootpath=pytester.path)
assert values[0].__file__.startswith(str(conf))
# and all sub paths get updated properly
values = conftest._getconftestmodules(p, importmode="prepend")
values = conftest._getconftestmodules(p, importmode="prepend", rootpath=pytester.path)
assert len(values) == 1
assert values[0].__file__.startswith(str(conf))

Expand All @@ -177,7 +196,7 @@ def test_conftestcutdir_inplace_considered(pytester: Pytester) -> None:
conf = pytester.makeconftest("")
conftest = PytestPluginManager()
conftest_setinitial(conftest, [conf.parent], confcutdir=conf.parent)
values = conftest._getconftestmodules(conf.parent, importmode="prepend")
values = conftest._getconftestmodules(conf.parent, importmode="prepend", rootpath=pytester.path)
assert len(values) == 1
assert values[0].__file__.startswith(str(conf))

Expand Down Expand Up @@ -347,15 +366,17 @@ def test_conftest_import_order(pytester: Pytester, monkeypatch: MonkeyPatch) ->
ct2 = sub / "conftest.py"
ct2.write_text("")

def impct(p, importmode):
def impct(p, importmode, root):
return p

conftest = PytestPluginManager()
conftest._confcutdir = pytester.path
monkeypatch.setattr(conftest, "_importconftest", impct)
mods = cast(List[Path], conftest._getconftestmodules(sub, importmode="prepend"))
mods = cast(List[Path], conftest._getconftestmodules(sub, importmode="prepend", rootpath=pytester.path))
expected = [ct1, ct2]
assert mods == expected
assert conftest._getconftestmodules(sub, importmode="prepend", rootpath=pytester.path) == [ct1, ct2]



def test_fixture_dependency(pytester: Pytester) -> None:
Expand Down
Loading