Add consider_namespace_packages ini option

Fix #11475
This commit is contained in:
Bruno Oliveira 2024-03-02 11:46:54 -03:00
parent 199d4e2b73
commit 111c0d910e
10 changed files with 315 additions and 77 deletions

View File

@ -547,6 +547,8 @@ class PytestPluginManager(PluginManager):
confcutdir: Optional[Path], confcutdir: Optional[Path],
invocation_dir: Path, invocation_dir: Path,
importmode: Union[ImportMode, str], importmode: Union[ImportMode, str],
*,
consider_namespace_packages: bool,
) -> None: ) -> None:
"""Load initial conftest files given a preparsed "namespace". """Load initial conftest files given a preparsed "namespace".
@ -572,10 +574,20 @@ class PytestPluginManager(PluginManager):
# Ensure we do not break if what appears to be an anchor # Ensure we do not break if what appears to be an anchor
# is in fact a very long option (#10169, #11394). # is in fact a very long option (#10169, #11394).
if safe_exists(anchor): if safe_exists(anchor):
self._try_load_conftest(anchor, importmode, rootpath) self._try_load_conftest(
anchor,
importmode,
rootpath,
consider_namespace_packages=consider_namespace_packages,
)
foundanchor = True foundanchor = True
if not foundanchor: if not foundanchor:
self._try_load_conftest(invocation_dir, importmode, rootpath) self._try_load_conftest(
invocation_dir,
importmode,
rootpath,
consider_namespace_packages=consider_namespace_packages,
)
def _is_in_confcutdir(self, path: Path) -> bool: def _is_in_confcutdir(self, path: Path) -> bool:
"""Whether to consider the given path to load conftests from.""" """Whether to consider the given path to load conftests from."""
@ -593,17 +605,37 @@ class PytestPluginManager(PluginManager):
return path not in self._confcutdir.parents return path not in self._confcutdir.parents
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,
*,
consider_namespace_packages: bool,
) -> None: ) -> None:
self._loadconftestmodules(anchor, importmode, rootpath) self._loadconftestmodules(
anchor,
importmode,
rootpath,
consider_namespace_packages=consider_namespace_packages,
)
# let's also consider test* subdirs # let's also consider test* subdirs
if anchor.is_dir(): if anchor.is_dir():
for x in anchor.glob("test*"): for x in anchor.glob("test*"):
if x.is_dir(): if x.is_dir():
self._loadconftestmodules(x, importmode, rootpath) self._loadconftestmodules(
x,
importmode,
rootpath,
consider_namespace_packages=consider_namespace_packages,
)
def _loadconftestmodules( def _loadconftestmodules(
self, path: Path, importmode: Union[str, ImportMode], rootpath: Path self,
path: Path,
importmode: Union[str, ImportMode],
rootpath: Path,
*,
consider_namespace_packages: bool,
) -> None: ) -> None:
if self._noconftest: if self._noconftest:
return return
@ -620,7 +652,12 @@ class PytestPluginManager(PluginManager):
if self._is_in_confcutdir(parent): if self._is_in_confcutdir(parent):
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,
consider_namespace_packages=consider_namespace_packages,
)
clist.append(mod) clist.append(mod)
self._dirpath2confmods[directory] = clist self._dirpath2confmods[directory] = clist
@ -642,7 +679,12 @@ class PytestPluginManager(PluginManager):
raise KeyError(name) raise KeyError(name)
def _importconftest( def _importconftest(
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path self,
conftestpath: Path,
importmode: Union[str, ImportMode],
rootpath: Path,
*,
consider_namespace_packages: bool,
) -> types.ModuleType: ) -> types.ModuleType:
conftestpath_plugin_name = str(conftestpath) conftestpath_plugin_name = str(conftestpath)
existing = self.get_plugin(conftestpath_plugin_name) existing = self.get_plugin(conftestpath_plugin_name)
@ -661,7 +703,12 @@ class PytestPluginManager(PluginManager):
pass pass
try: try:
mod = import_path(conftestpath, mode=importmode, root=rootpath) mod = import_path(
conftestpath,
mode=importmode,
root=rootpath,
consider_namespace_packages=consider_namespace_packages,
)
except Exception as e: except Exception as e:
assert e.__traceback__ is not None assert e.__traceback__ is not None
raise ConftestImportFailure(conftestpath, cause=e) from e raise ConftestImportFailure(conftestpath, cause=e) from e
@ -1177,6 +1224,9 @@ class Config:
confcutdir=early_config.known_args_namespace.confcutdir, confcutdir=early_config.known_args_namespace.confcutdir,
invocation_dir=early_config.invocation_params.dir, invocation_dir=early_config.invocation_params.dir,
importmode=early_config.known_args_namespace.importmode, importmode=early_config.known_args_namespace.importmode,
consider_namespace_packages=early_config.getini(
"consider_namespace_packages"
),
) )
def _initini(self, args: Sequence[str]) -> None: def _initini(self, args: Sequence[str]) -> None:

View File

@ -222,6 +222,12 @@ def pytest_addoption(parser: Parser) -> None:
help="Prepend/append to sys.path when importing test modules and conftest " help="Prepend/append to sys.path when importing test modules and conftest "
"files. Default: prepend.", "files. Default: prepend.",
) )
parser.addini(
"consider_namespace_packages",
type="bool",
default=False,
help="Consider namespace packages when resolving module names during import",
)
group = parser.getgroup("debugconfig", "test session debugging and configuration") group = parser.getgroup("debugconfig", "test session debugging and configuration")
group.addoption( group.addoption(

View File

@ -488,6 +488,7 @@ def import_path(
*, *,
mode: Union[str, ImportMode] = ImportMode.prepend, mode: Union[str, ImportMode] = ImportMode.prepend,
root: Path, root: Path,
consider_namespace_packages: bool,
) -> ModuleType: ) -> ModuleType:
""" """
Import and return a module from the given path, which can be a file (a module) or Import and return a module from the given path, which can be a file (a module) or
@ -515,6 +516,9 @@ def import_path(
a unique name for the module being imported so it can safely be stored a unique name for the module being imported so it can safely be stored
into ``sys.modules``. into ``sys.modules``.
:param consider_namespace_packages:
If True, consider namespace packages when resolving module names.
:raises ImportPathMismatchError: :raises ImportPathMismatchError:
If after importing the given `path` and the module `__file__` If after importing the given `path` and the module `__file__`
are different. Only raised in `prepend` and `append` modes. are different. Only raised in `prepend` and `append` modes.
@ -530,7 +534,7 @@ def import_path(
# without touching sys.path. # without touching sys.path.
try: try:
pkg_root, module_name = resolve_pkg_root_and_module_name( pkg_root, module_name = resolve_pkg_root_and_module_name(
path, consider_ns_packages=True path, consider_namespace_packages=consider_namespace_packages
) )
except CouldNotResolvePathError: except CouldNotResolvePathError:
pass pass
@ -556,7 +560,7 @@ def import_path(
try: try:
pkg_root, module_name = resolve_pkg_root_and_module_name( pkg_root, module_name = resolve_pkg_root_and_module_name(
path, consider_ns_packages=True path, consider_namespace_packages=consider_namespace_packages
) )
except CouldNotResolvePathError: except CouldNotResolvePathError:
pkg_root, module_name = path.parent, path.stem pkg_root, module_name = path.parent, path.stem
@ -674,7 +678,7 @@ def module_name_from_path(path: Path, root: Path) -> str:
# Module names cannot contain ".", normalize them to "_". This prevents # Module names cannot contain ".", normalize them to "_". This prevents
# a directory having a "." in the name (".env.310" for example) causing extra intermediate modules. # a directory having a "." in the name (".env.310" for example) causing extra intermediate modules.
# Also, important to replace "." at the start of paths, as those are considered relative imports. # Also, important to replace "." at the start of paths, as those are considered relative imports.
path_parts = [x.replace(".", "_") for x in path_parts] path_parts = tuple(x.replace(".", "_") for x in path_parts)
return ".".join(path_parts) return ".".join(path_parts)
@ -738,7 +742,7 @@ def resolve_package_path(path: Path) -> Optional[Path]:
def resolve_pkg_root_and_module_name( def resolve_pkg_root_and_module_name(
path: Path, *, consider_ns_packages: bool = False path: Path, *, consider_namespace_packages: bool = False
) -> Tuple[Path, str]: ) -> Tuple[Path, str]:
""" """
Return the path to the directory of the root package that contains the Return the path to the directory of the root package that contains the
@ -753,7 +757,7 @@ def resolve_pkg_root_and_module_name(
Passing the full path to `models.py` will yield Path("src") and "app.core.models". Passing the full path to `models.py` will yield Path("src") and "app.core.models".
If consider_ns_packages is True, then we additionally check upwards in the hierarchy If consider_namespace_packages is True, then we additionally check upwards in the hierarchy
until we find a directory that is reachable from sys.path, which marks it as a namespace package: until we find a directory that is reachable from sys.path, which marks it as a namespace package:
https://packaging.python.org/en/latest/guides/packaging-namespace-packages https://packaging.python.org/en/latest/guides/packaging-namespace-packages
@ -764,7 +768,7 @@ def resolve_pkg_root_and_module_name(
if pkg_path is not None: if pkg_path is not None:
pkg_root = pkg_path.parent pkg_root = pkg_path.parent
# https://packaging.python.org/en/latest/guides/packaging-namespace-packages/ # https://packaging.python.org/en/latest/guides/packaging-namespace-packages/
if consider_ns_packages: if consider_namespace_packages:
# Go upwards in the hierarchy, if we find a parent path included # Go upwards in the hierarchy, if we find a parent path included
# in sys.path, it means the package found by resolve_package_path() # in sys.path, it means the package found by resolve_package_path()
# actually belongs to a namespace package. # actually belongs to a namespace package.

View File

@ -516,7 +516,12 @@ def importtestmodule(
# We assume we are only called once per module. # We assume we are only called once per module.
importmode = config.getoption("--import-mode") importmode = config.getoption("--import-mode")
try: try:
mod = import_path(path, mode=importmode, root=config.rootpath) mod = import_path(
path,
mode=importmode,
root=config.rootpath,
consider_namespace_packages=config.getini("consider_namespace_packages"),
)
except SyntaxError as e: except SyntaxError as e:
raise nodes.Collector.CollectError( raise nodes.Collector.CollectError(
ExceptionInfo.from_current().getrepr(style="short") ExceptionInfo.from_current().getrepr(style="short")

View File

@ -380,6 +380,9 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport:
collector.path, collector.path,
collector.config.getoption("importmode"), collector.config.getoption("importmode"),
rootpath=collector.config.rootpath, rootpath=collector.config.rootpath,
consider_namespace_packages=collector.config.getini(
"consider_namespace_packages"
),
) )
return list(collector.collect()) return list(collector.collect())

View File

@ -180,7 +180,7 @@ class TestTraceback_f_g_h:
def test_traceback_cut_excludepath(self, pytester: Pytester) -> None: def test_traceback_cut_excludepath(self, pytester: Pytester) -> None:
p = pytester.makepyfile("def f(): raise ValueError") p = pytester.makepyfile("def f(): raise ValueError")
with pytest.raises(ValueError) as excinfo: with pytest.raises(ValueError) as excinfo:
import_path(p, root=pytester.path).f() # type: ignore[attr-defined] import_path(p, root=pytester.path, consider_namespace_packages=False).f() # type: ignore[attr-defined]
basedir = Path(pytest.__file__).parent basedir = Path(pytest.__file__).parent
newtraceback = excinfo.traceback.cut(excludepath=basedir) newtraceback = excinfo.traceback.cut(excludepath=basedir)
for x in newtraceback: for x in newtraceback:
@ -543,7 +543,9 @@ class TestFormattedExcinfo:
tmp_path.joinpath("__init__.py").touch() tmp_path.joinpath("__init__.py").touch()
modpath.write_text(source, encoding="utf-8") modpath.write_text(source, encoding="utf-8")
importlib.invalidate_caches() importlib.invalidate_caches()
return import_path(modpath, root=tmp_path) return import_path(
modpath, root=tmp_path, consider_namespace_packages=False
)
return importasmod return importasmod

View File

@ -296,7 +296,7 @@ def test_source_of_class_at_eof_without_newline(_sys_snapshot, tmp_path: Path) -
) )
path = tmp_path.joinpath("a.py") path = tmp_path.joinpath("a.py")
path.write_text(str(source), encoding="utf-8") path.write_text(str(source), encoding="utf-8")
mod: Any = import_path(path, root=tmp_path) mod: Any = import_path(path, root=tmp_path, consider_namespace_packages=False)
s2 = Source(mod.A) s2 = Source(mod.A)
assert str(source).strip() == str(s2).strip() assert str(source).strip() == str(s2).strip()

View File

@ -38,6 +38,7 @@ def conftest_setinitial(
confcutdir=confcutdir, confcutdir=confcutdir,
invocation_dir=Path.cwd(), invocation_dir=Path.cwd(),
importmode="prepend", importmode="prepend",
consider_namespace_packages=False,
) )
@ -64,7 +65,9 @@ class TestConftestValueAccessGlobal:
def test_basic_init(self, basedir: Path) -> None: def test_basic_init(self, basedir: Path) -> None:
conftest = PytestPluginManager() conftest = PytestPluginManager()
p = basedir / "adir" p = basedir / "adir"
conftest._loadconftestmodules(p, importmode="prepend", rootpath=basedir) conftest._loadconftestmodules(
p, importmode="prepend", rootpath=basedir, consider_namespace_packages=False
)
assert conftest._rget_with_confmod("a", p)[1] == 1 assert conftest._rget_with_confmod("a", p)[1] == 1
def test_immediate_initialiation_and_incremental_are_the_same( def test_immediate_initialiation_and_incremental_are_the_same(
@ -72,15 +75,26 @@ class TestConftestValueAccessGlobal:
) -> None: ) -> None:
conftest = PytestPluginManager() conftest = PytestPluginManager()
assert not len(conftest._dirpath2confmods) assert not len(conftest._dirpath2confmods)
conftest._loadconftestmodules(basedir, importmode="prepend", rootpath=basedir) conftest._loadconftestmodules(
basedir,
importmode="prepend",
rootpath=basedir,
consider_namespace_packages=False,
)
snap1 = len(conftest._dirpath2confmods) snap1 = len(conftest._dirpath2confmods)
assert snap1 == 1 assert snap1 == 1
conftest._loadconftestmodules( conftest._loadconftestmodules(
basedir / "adir", importmode="prepend", rootpath=basedir basedir / "adir",
importmode="prepend",
rootpath=basedir,
consider_namespace_packages=False,
) )
assert len(conftest._dirpath2confmods) == snap1 + 1 assert len(conftest._dirpath2confmods) == snap1 + 1
conftest._loadconftestmodules( conftest._loadconftestmodules(
basedir / "b", importmode="prepend", rootpath=basedir basedir / "b",
importmode="prepend",
rootpath=basedir,
consider_namespace_packages=False,
) )
assert len(conftest._dirpath2confmods) == snap1 + 2 assert len(conftest._dirpath2confmods) == snap1 + 2
@ -92,10 +106,18 @@ class TestConftestValueAccessGlobal:
def test_value_access_by_path(self, basedir: Path) -> None: def test_value_access_by_path(self, basedir: Path) -> None:
conftest = ConftestWithSetinitial(basedir) conftest = ConftestWithSetinitial(basedir)
adir = basedir / "adir" adir = basedir / "adir"
conftest._loadconftestmodules(adir, importmode="prepend", rootpath=basedir) conftest._loadconftestmodules(
adir,
importmode="prepend",
rootpath=basedir,
consider_namespace_packages=False,
)
assert conftest._rget_with_confmod("a", adir)[1] == 1 assert conftest._rget_with_confmod("a", adir)[1] == 1
conftest._loadconftestmodules( conftest._loadconftestmodules(
adir / "b", importmode="prepend", rootpath=basedir adir / "b",
importmode="prepend",
rootpath=basedir,
consider_namespace_packages=False,
) )
assert conftest._rget_with_confmod("a", adir / "b")[1] == 1.5 assert conftest._rget_with_confmod("a", adir / "b")[1] == 1.5
@ -152,7 +174,12 @@ def test_conftest_global_import(pytester: Pytester) -> None:
import pytest import pytest
from _pytest.config import PytestPluginManager from _pytest.config import PytestPluginManager
conf = PytestPluginManager() conf = PytestPluginManager()
mod = conf._importconftest(Path("conftest.py"), importmode="prepend", rootpath=Path.cwd()) mod = conf._importconftest(
Path("conftest.py"),
importmode="prepend",
rootpath=Path.cwd(),
consider_namespace_packages=False,
)
assert mod.x == 3 assert mod.x == 3
import conftest import conftest
assert conftest is mod, (conftest, mod) assert conftest is mod, (conftest, mod)
@ -160,7 +187,12 @@ def test_conftest_global_import(pytester: Pytester) -> None:
sub.mkdir() sub.mkdir()
subconf = sub / "conftest.py" subconf = sub / "conftest.py"
subconf.write_text("y=4", encoding="utf-8") subconf.write_text("y=4", encoding="utf-8")
mod2 = conf._importconftest(subconf, importmode="prepend", rootpath=Path.cwd()) mod2 = conf._importconftest(
subconf,
importmode="prepend",
rootpath=Path.cwd(),
consider_namespace_packages=False,
)
assert mod != mod2 assert mod != mod2
assert mod2.y == 4 assert mod2.y == 4
import conftest import conftest
@ -176,17 +208,30 @@ def test_conftestcutdir(pytester: Pytester) -> None:
p = pytester.mkdir("x") p = pytester.mkdir("x")
conftest = PytestPluginManager() conftest = PytestPluginManager()
conftest_setinitial(conftest, [pytester.path], confcutdir=p) conftest_setinitial(conftest, [pytester.path], confcutdir=p)
conftest._loadconftestmodules(p, importmode="prepend", rootpath=pytester.path) conftest._loadconftestmodules(
p,
importmode="prepend",
rootpath=pytester.path,
consider_namespace_packages=False,
)
values = conftest._getconftestmodules(p) values = conftest._getconftestmodules(p)
assert len(values) == 0 assert len(values) == 0
conftest._loadconftestmodules( conftest._loadconftestmodules(
conf.parent, importmode="prepend", rootpath=pytester.path conf.parent,
importmode="prepend",
rootpath=pytester.path,
consider_namespace_packages=False,
) )
values = conftest._getconftestmodules(conf.parent) values = conftest._getconftestmodules(conf.parent)
assert len(values) == 0 assert len(values) == 0
assert not conftest.has_plugin(str(conf)) 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,
consider_namespace_packages=False,
)
values = conftest._getconftestmodules(conf.parent) values = conftest._getconftestmodules(conf.parent)
assert values[0].__file__ is not None assert values[0].__file__ is not None
assert values[0].__file__.startswith(str(conf)) assert values[0].__file__.startswith(str(conf))
@ -405,13 +450,18 @@ def test_conftest_import_order(pytester: Pytester, monkeypatch: MonkeyPatch) ->
ct2 = sub / "conftest.py" ct2 = sub / "conftest.py"
ct2.write_text("", encoding="utf-8") ct2.write_text("", encoding="utf-8")
def impct(p, importmode, root): def impct(p, importmode, root, consider_namespace_packages):
return p return p
conftest = PytestPluginManager() conftest = PytestPluginManager()
conftest._confcutdir = pytester.path conftest._confcutdir = pytester.path
monkeypatch.setattr(conftest, "_importconftest", impct) monkeypatch.setattr(conftest, "_importconftest", impct)
conftest._loadconftestmodules(sub, importmode="prepend", rootpath=pytester.path) conftest._loadconftestmodules(
sub,
importmode="prepend",
rootpath=pytester.path,
consider_namespace_packages=False,
)
mods = cast(List[Path], conftest._getconftestmodules(sub)) mods = cast(List[Path], conftest._getconftestmodules(sub))
expected = [ct1, ct2] expected = [ct1, ct2]
assert mods == expected assert mods == expected

View File

@ -171,13 +171,17 @@ class TestImportPath:
) )
def test_smoke_test(self, path1: Path) -> None: def test_smoke_test(self, path1: Path) -> None:
obj = import_path(path1 / "execfile.py", root=path1) obj = import_path(
path1 / "execfile.py", root=path1, consider_namespace_packages=False
)
assert obj.x == 42 # type: ignore[attr-defined] assert obj.x == 42 # type: ignore[attr-defined]
assert obj.__name__ == "execfile" assert obj.__name__ == "execfile"
def test_import_path_missing_file(self, path1: Path) -> None: def test_import_path_missing_file(self, path1: Path) -> None:
with pytest.raises(ImportPathMismatchError): with pytest.raises(ImportPathMismatchError):
import_path(path1 / "sampledir", root=path1) import_path(
path1 / "sampledir", root=path1, consider_namespace_packages=False
)
def test_renamed_dir_creates_mismatch( def test_renamed_dir_creates_mismatch(
self, tmp_path: Path, monkeypatch: MonkeyPatch self, tmp_path: Path, monkeypatch: MonkeyPatch
@ -185,25 +189,37 @@ class TestImportPath:
tmp_path.joinpath("a").mkdir() tmp_path.joinpath("a").mkdir()
p = tmp_path.joinpath("a", "test_x123.py") p = tmp_path.joinpath("a", "test_x123.py")
p.touch() p.touch()
import_path(p, root=tmp_path) import_path(p, root=tmp_path, consider_namespace_packages=False)
tmp_path.joinpath("a").rename(tmp_path.joinpath("b")) tmp_path.joinpath("a").rename(tmp_path.joinpath("b"))
with pytest.raises(ImportPathMismatchError): with pytest.raises(ImportPathMismatchError):
import_path(tmp_path.joinpath("b", "test_x123.py"), root=tmp_path) import_path(
tmp_path.joinpath("b", "test_x123.py"),
root=tmp_path,
consider_namespace_packages=False,
)
# Errors can be ignored. # Errors can be ignored.
monkeypatch.setenv("PY_IGNORE_IMPORTMISMATCH", "1") monkeypatch.setenv("PY_IGNORE_IMPORTMISMATCH", "1")
import_path(tmp_path.joinpath("b", "test_x123.py"), root=tmp_path) import_path(
tmp_path.joinpath("b", "test_x123.py"),
root=tmp_path,
consider_namespace_packages=False,
)
# PY_IGNORE_IMPORTMISMATCH=0 does not ignore error. # PY_IGNORE_IMPORTMISMATCH=0 does not ignore error.
monkeypatch.setenv("PY_IGNORE_IMPORTMISMATCH", "0") monkeypatch.setenv("PY_IGNORE_IMPORTMISMATCH", "0")
with pytest.raises(ImportPathMismatchError): with pytest.raises(ImportPathMismatchError):
import_path(tmp_path.joinpath("b", "test_x123.py"), root=tmp_path) import_path(
tmp_path.joinpath("b", "test_x123.py"),
root=tmp_path,
consider_namespace_packages=False,
)
def test_messy_name(self, tmp_path: Path) -> None: def test_messy_name(self, tmp_path: Path) -> None:
# https://bitbucket.org/hpk42/py-trunk/issue/129 # https://bitbucket.org/hpk42/py-trunk/issue/129
path = tmp_path / "foo__init__.py" path = tmp_path / "foo__init__.py"
path.touch() path.touch()
module = import_path(path, root=tmp_path) module = import_path(path, root=tmp_path, consider_namespace_packages=False)
assert module.__name__ == "foo__init__" assert module.__name__ == "foo__init__"
def test_dir(self, tmp_path: Path) -> None: def test_dir(self, tmp_path: Path) -> None:
@ -211,31 +227,39 @@ class TestImportPath:
p.mkdir() p.mkdir()
p_init = p / "__init__.py" p_init = p / "__init__.py"
p_init.touch() p_init.touch()
m = import_path(p, root=tmp_path) m = import_path(p, root=tmp_path, consider_namespace_packages=False)
assert m.__name__ == "hello_123" assert m.__name__ == "hello_123"
m = import_path(p_init, root=tmp_path) m = import_path(p_init, root=tmp_path, consider_namespace_packages=False)
assert m.__name__ == "hello_123" assert m.__name__ == "hello_123"
def test_a(self, path1: Path) -> None: def test_a(self, path1: Path) -> None:
otherdir = path1 / "otherdir" otherdir = path1 / "otherdir"
mod = import_path(otherdir / "a.py", root=path1) mod = import_path(
otherdir / "a.py", root=path1, consider_namespace_packages=False
)
assert mod.result == "got it" # type: ignore[attr-defined] assert mod.result == "got it" # type: ignore[attr-defined]
assert mod.__name__ == "otherdir.a" assert mod.__name__ == "otherdir.a"
def test_b(self, path1: Path) -> None: def test_b(self, path1: Path) -> None:
otherdir = path1 / "otherdir" otherdir = path1 / "otherdir"
mod = import_path(otherdir / "b.py", root=path1) mod = import_path(
otherdir / "b.py", root=path1, consider_namespace_packages=False
)
assert mod.stuff == "got it" # type: ignore[attr-defined] assert mod.stuff == "got it" # type: ignore[attr-defined]
assert mod.__name__ == "otherdir.b" assert mod.__name__ == "otherdir.b"
def test_c(self, path1: Path) -> None: def test_c(self, path1: Path) -> None:
otherdir = path1 / "otherdir" otherdir = path1 / "otherdir"
mod = import_path(otherdir / "c.py", root=path1) mod = import_path(
otherdir / "c.py", root=path1, consider_namespace_packages=False
)
assert mod.value == "got it" # type: ignore[attr-defined] assert mod.value == "got it" # type: ignore[attr-defined]
def test_d(self, path1: Path) -> None: def test_d(self, path1: Path) -> None:
otherdir = path1 / "otherdir" otherdir = path1 / "otherdir"
mod = import_path(otherdir / "d.py", root=path1) mod = import_path(
otherdir / "d.py", root=path1, consider_namespace_packages=False
)
assert mod.value2 == "got it" # type: ignore[attr-defined] assert mod.value2 == "got it" # type: ignore[attr-defined]
def test_import_after(self, tmp_path: Path) -> None: def test_import_after(self, tmp_path: Path) -> None:
@ -243,7 +267,7 @@ class TestImportPath:
tmp_path.joinpath("xxxpackage", "__init__.py").touch() tmp_path.joinpath("xxxpackage", "__init__.py").touch()
mod1path = tmp_path.joinpath("xxxpackage", "module1.py") mod1path = tmp_path.joinpath("xxxpackage", "module1.py")
mod1path.touch() mod1path.touch()
mod1 = import_path(mod1path, root=tmp_path) mod1 = import_path(mod1path, root=tmp_path, consider_namespace_packages=False)
assert mod1.__name__ == "xxxpackage.module1" assert mod1.__name__ == "xxxpackage.module1"
from xxxpackage import module1 from xxxpackage import module1
@ -262,7 +286,9 @@ class TestImportPath:
pseudopath.touch() pseudopath.touch()
mod.__file__ = str(pseudopath) mod.__file__ = str(pseudopath)
mp.setitem(sys.modules, name, mod) mp.setitem(sys.modules, name, mod)
newmod = import_path(p, root=tmp_path) newmod = import_path(
p, root=tmp_path, consider_namespace_packages=False
)
assert mod == newmod assert mod == newmod
mod = ModuleType(name) mod = ModuleType(name)
pseudopath = tmp_path.joinpath(name + "123.py") pseudopath = tmp_path.joinpath(name + "123.py")
@ -270,7 +296,7 @@ class TestImportPath:
mod.__file__ = str(pseudopath) mod.__file__ = str(pseudopath)
monkeypatch.setitem(sys.modules, name, mod) monkeypatch.setitem(sys.modules, name, mod)
with pytest.raises(ImportPathMismatchError) as excinfo: with pytest.raises(ImportPathMismatchError) as excinfo:
import_path(p, root=tmp_path) import_path(p, root=tmp_path, consider_namespace_packages=False)
modname, modfile, orig = excinfo.value.args modname, modfile, orig = excinfo.value.args
assert modname == name assert modname == name
assert modfile == str(pseudopath) assert modfile == str(pseudopath)
@ -283,13 +309,19 @@ class TestImportPath:
file1 = root1 / "x123.py" file1 = root1 / "x123.py"
file1.touch() file1.touch()
assert str(root1) not in sys.path assert str(root1) not in sys.path
import_path(file1, mode="append", root=tmp_path) import_path(
file1, mode="append", root=tmp_path, consider_namespace_packages=False
)
assert str(root1) == sys.path[-1] assert str(root1) == sys.path[-1]
assert str(root1) not in sys.path[:-1] assert str(root1) not in sys.path[:-1]
def test_invalid_path(self, tmp_path: Path) -> None: def test_invalid_path(self, tmp_path: Path) -> None:
with pytest.raises(ImportError): with pytest.raises(ImportError):
import_path(tmp_path / "invalid.py", root=tmp_path) import_path(
tmp_path / "invalid.py",
root=tmp_path,
consider_namespace_packages=False,
)
@pytest.fixture @pytest.fixture
def simple_module( def simple_module(
@ -307,7 +339,12 @@ class TestImportPath:
self, simple_module: Path, tmp_path: Path, request: pytest.FixtureRequest self, simple_module: Path, tmp_path: Path, request: pytest.FixtureRequest
) -> None: ) -> None:
"""`importlib` mode does not change sys.path.""" """`importlib` mode does not change sys.path."""
module = import_path(simple_module, mode="importlib", root=tmp_path) module = import_path(
simple_module,
mode="importlib",
root=tmp_path,
consider_namespace_packages=False,
)
assert module.foo(2) == 42 # type: ignore[attr-defined] assert module.foo(2) == 42 # type: ignore[attr-defined]
assert str(simple_module.parent) not in sys.path assert str(simple_module.parent) not in sys.path
assert module.__name__ in sys.modules assert module.__name__ in sys.modules
@ -319,8 +356,18 @@ class TestImportPath:
self, simple_module: Path, tmp_path: Path self, simple_module: Path, tmp_path: Path
) -> None: ) -> None:
"""`importlib` mode called remembers previous module (#10341, #10811).""" """`importlib` mode called remembers previous module (#10341, #10811)."""
module1 = import_path(simple_module, mode="importlib", root=tmp_path) module1 = import_path(
module2 = import_path(simple_module, mode="importlib", root=tmp_path) simple_module,
mode="importlib",
root=tmp_path,
consider_namespace_packages=False,
)
module2 = import_path(
simple_module,
mode="importlib",
root=tmp_path,
consider_namespace_packages=False,
)
assert module1 is module2 assert module1 is module2
def test_no_meta_path_found( def test_no_meta_path_found(
@ -328,7 +375,12 @@ class TestImportPath:
) -> None: ) -> None:
"""Even without any meta_path should still import module.""" """Even without any meta_path should still import module."""
monkeypatch.setattr(sys, "meta_path", []) monkeypatch.setattr(sys, "meta_path", [])
module = import_path(simple_module, mode="importlib", root=tmp_path) module = import_path(
simple_module,
mode="importlib",
root=tmp_path,
consider_namespace_packages=False,
)
assert module.foo(2) == 42 # type: ignore[attr-defined] assert module.foo(2) == 42 # type: ignore[attr-defined]
# mode='importlib' fails if no spec is found to load the module # mode='importlib' fails if no spec is found to load the module
@ -341,7 +393,12 @@ class TestImportPath:
importlib.util, "spec_from_file_location", lambda *args: None importlib.util, "spec_from_file_location", lambda *args: None
) )
with pytest.raises(ImportError): with pytest.raises(ImportError):
import_path(simple_module, mode="importlib", root=tmp_path) import_path(
simple_module,
mode="importlib",
root=tmp_path,
consider_namespace_packages=False,
)
def test_resolve_package_path(tmp_path: Path) -> None: def test_resolve_package_path(tmp_path: Path) -> None:
@ -477,7 +534,9 @@ def test_samefile_false_negatives(tmp_path: Path, monkeypatch: MonkeyPatch) -> N
# the paths too. Using a context to narrow the patch as much as possible given # the paths too. Using a context to narrow the patch as much as possible given
# this is an important system function. # this is an important system function.
mp.setattr(os.path, "samefile", lambda x, y: False) mp.setattr(os.path, "samefile", lambda x, y: False)
module = import_path(module_path, root=tmp_path) module = import_path(
module_path, root=tmp_path, consider_namespace_packages=False
)
assert getattr(module, "foo")() == 42 assert getattr(module, "foo")() == 42
@ -499,7 +558,9 @@ class TestImportLibMode:
encoding="utf-8", encoding="utf-8",
) )
module = import_path(fn, mode="importlib", root=tmp_path) module = import_path(
fn, mode="importlib", root=tmp_path, consider_namespace_packages=False
)
Data: Any = getattr(module, "Data") Data: Any = getattr(module, "Data")
data = Data(value="foo") data = Data(value="foo")
assert data.value == "foo" assert data.value == "foo"
@ -525,7 +586,9 @@ class TestImportLibMode:
encoding="utf-8", encoding="utf-8",
) )
module = import_path(fn, mode="importlib", root=tmp_path) module = import_path(
fn, mode="importlib", root=tmp_path, consider_namespace_packages=False
)
round_trip = getattr(module, "round_trip") round_trip = getattr(module, "round_trip")
action = round_trip() action = round_trip()
assert action() == 42 assert action() == 42
@ -575,10 +638,14 @@ class TestImportLibMode:
s = pickle.dumps(obj) s = pickle.dumps(obj)
return pickle.loads(s) return pickle.loads(s)
module = import_path(fn1, mode="importlib", root=tmp_path) module = import_path(
fn1, mode="importlib", root=tmp_path, consider_namespace_packages=False
)
Data1 = getattr(module, "Data") Data1 = getattr(module, "Data")
module = import_path(fn2, mode="importlib", root=tmp_path) module = import_path(
fn2, mode="importlib", root=tmp_path, consider_namespace_packages=False
)
Data2 = getattr(module, "Data") Data2 = getattr(module, "Data")
assert round_trip(Data1(20)) == Data1(20) assert round_trip(Data1(20)) == Data1(20)
@ -635,7 +702,7 @@ class TestImportLibMode:
# If we add tmp_path to sys.path, src becomes a namespace package. # If we add tmp_path to sys.path, src becomes a namespace package.
monkeypatch.syspath_prepend(tmp_path) monkeypatch.syspath_prepend(tmp_path)
assert resolve_pkg_root_and_module_name( assert resolve_pkg_root_and_module_name(
models_py, consider_ns_packages=True models_py, consider_namespace_packages=True
) == ( ) == (
tmp_path, tmp_path,
"src.app.core.models", "src.app.core.models",
@ -709,7 +776,12 @@ class TestImportLibMode:
encoding="ascii", encoding="ascii",
) )
mod = import_path(init, root=tmp_path, mode=ImportMode.importlib) mod = import_path(
init,
root=tmp_path,
mode=ImportMode.importlib,
consider_namespace_packages=False,
)
assert len(mod.instance.INSTANCES) == 1 assert len(mod.instance.INSTANCES) == 1
def test_importlib_root_is_package(self, pytester: Pytester) -> None: def test_importlib_root_is_package(self, pytester: Pytester) -> None:
@ -828,16 +900,31 @@ class TestImportLibMode:
) )
# core_py is reached from sys.path, so should be imported normally. # core_py is reached from sys.path, so should be imported normally.
mod = import_path(core_py, mode="importlib", root=pytester.path) mod = import_path(
core_py,
mode="importlib",
root=pytester.path,
consider_namespace_packages=False,
)
assert mod.__name__ == "app.core" assert mod.__name__ == "app.core"
assert mod.__file__ and Path(mod.__file__) == core_py assert mod.__file__ and Path(mod.__file__) == core_py
# tests are not reachable from sys.path, so they are imported as a standalone modules. # tests are not reachable from sys.path, so they are imported as a standalone modules.
# Instead of '.tests.a.test_core', we import as "_tests.a.test_core" because # Instead of '.tests.a.test_core', we import as "_tests.a.test_core" because
# importlib considers module names starting with '.' to be local imports. # importlib considers module names starting with '.' to be local imports.
mod = import_path(test_path1, mode="importlib", root=pytester.path) mod = import_path(
test_path1,
mode="importlib",
root=pytester.path,
consider_namespace_packages=False,
)
assert mod.__name__ == "_tests.a.test_core" assert mod.__name__ == "_tests.a.test_core"
mod = import_path(test_path2, mode="importlib", root=pytester.path) mod = import_path(
test_path2,
mode="importlib",
root=pytester.path,
consider_namespace_packages=False,
)
assert mod.__name__ == "_tests.b.test_core" assert mod.__name__ == "_tests.b.test_core"
def test_import_using_normal_mechanism_first_integration( def test_import_using_normal_mechanism_first_integration(
@ -889,14 +976,22 @@ class TestImportLibMode:
# The 'x.py' module from sys.path was not imported for sure because # The 'x.py' module from sys.path was not imported for sure because
# otherwise we would get an AssertionError. # otherwise we would get an AssertionError.
mod = import_path( mod = import_path(
x_in_sub_folder, mode=ImportMode.importlib, root=pytester.path x_in_sub_folder,
mode=ImportMode.importlib,
root=pytester.path,
consider_namespace_packages=False,
) )
assert mod.__file__ and Path(mod.__file__) == x_in_sub_folder assert mod.__file__ and Path(mod.__file__) == x_in_sub_folder
assert mod.X == "a/b/x" assert mod.X == "a/b/x"
# Attempt to import root 'x.py'. # Attempt to import root 'x.py'.
with pytest.raises(AssertionError, match="x at root"): with pytest.raises(AssertionError, match="x at root"):
_ = import_path(x_at_root, mode=ImportMode.importlib, root=pytester.path) _ = import_path(
x_at_root,
mode=ImportMode.importlib,
root=pytester.path,
consider_namespace_packages=False,
)
def test_safe_exists(tmp_path: Path) -> None: def test_safe_exists(tmp_path: Path) -> None:
@ -979,26 +1074,33 @@ class TestNamespacePackages:
) )
pkg_root, module_name = resolve_pkg_root_and_module_name( pkg_root, module_name = resolve_pkg_root_and_module_name(
models_py, consider_ns_packages=True models_py, consider_namespace_packages=True
) )
assert (pkg_root, module_name) == ( assert (pkg_root, module_name) == (
tmp_path / "src/dist1", tmp_path / "src/dist1",
"com.company.app.core.models", "com.company.app.core.models",
) )
mod = import_path(models_py, mode=import_mode, root=tmp_path) mod = import_path(
models_py, mode=import_mode, root=tmp_path, consider_namespace_packages=True
)
assert mod.__name__ == "com.company.app.core.models" assert mod.__name__ == "com.company.app.core.models"
assert mod.__file__ == str(models_py) assert mod.__file__ == str(models_py)
pkg_root, module_name = resolve_pkg_root_and_module_name( pkg_root, module_name = resolve_pkg_root_and_module_name(
algorithms_py, consider_ns_packages=True algorithms_py, consider_namespace_packages=True
) )
assert (pkg_root, module_name) == ( assert (pkg_root, module_name) == (
tmp_path / "src/dist2", tmp_path / "src/dist2",
"com.company.calc.algo.algorithms", "com.company.calc.algo.algorithms",
) )
mod = import_path(algorithms_py, mode=import_mode, root=tmp_path) mod = import_path(
algorithms_py,
mode=import_mode,
root=tmp_path,
consider_namespace_packages=True,
)
assert mod.__name__ == "com.company.calc.algo.algorithms" assert mod.__name__ == "com.company.calc.algo.algorithms"
assert mod.__file__ == str(algorithms_py) assert mod.__file__ == str(algorithms_py)
@ -1019,7 +1121,7 @@ class TestNamespacePackages:
(tmp_path / "src/dist1/com/__init__.py").touch() (tmp_path / "src/dist1/com/__init__.py").touch()
pkg_root, module_name = resolve_pkg_root_and_module_name( pkg_root, module_name = resolve_pkg_root_and_module_name(
models_py, consider_ns_packages=True models_py, consider_namespace_packages=True
) )
assert (pkg_root, module_name) == ( assert (pkg_root, module_name) == (
tmp_path / "src/dist1/com/company", tmp_path / "src/dist1/com/company",

View File

@ -46,7 +46,10 @@ class TestPytestPluginInteractions:
kwargs=dict(pluginmanager=config.pluginmanager) kwargs=dict(pluginmanager=config.pluginmanager)
) )
config.pluginmanager._importconftest( config.pluginmanager._importconftest(
conf, importmode="prepend", rootpath=pytester.path conf,
importmode="prepend",
rootpath=pytester.path,
consider_namespace_packages=False,
) )
# print(config.pluginmanager.get_plugins()) # print(config.pluginmanager.get_plugins())
res = config.hook.pytest_myhook(xyz=10) res = config.hook.pytest_myhook(xyz=10)
@ -75,7 +78,10 @@ class TestPytestPluginInteractions:
""" """
) )
config.pluginmanager._importconftest( config.pluginmanager._importconftest(
p, importmode="prepend", rootpath=pytester.path p,
importmode="prepend",
rootpath=pytester.path,
consider_namespace_packages=False,
) )
assert config.option.test123 assert config.option.test123
@ -115,6 +121,7 @@ class TestPytestPluginInteractions:
conftest, conftest,
importmode="prepend", importmode="prepend",
rootpath=pytester.path, rootpath=pytester.path,
consider_namespace_packages=False,
) )
plugin = config.pluginmanager.get_plugin(str(conftest)) plugin = config.pluginmanager.get_plugin(str(conftest))
assert plugin is mod assert plugin is mod
@ -123,6 +130,7 @@ class TestPytestPluginInteractions:
conftest_upper_case, conftest_upper_case,
importmode="prepend", importmode="prepend",
rootpath=pytester.path, rootpath=pytester.path,
consider_namespace_packages=False,
) )
plugin_uppercase = config.pluginmanager.get_plugin(str(conftest_upper_case)) plugin_uppercase = config.pluginmanager.get_plugin(str(conftest_upper_case))
assert plugin_uppercase is mod_uppercase assert plugin_uppercase is mod_uppercase
@ -174,12 +182,18 @@ class TestPytestPluginInteractions:
conftest2 = pytester.path.joinpath("tests/subdir/conftest.py") conftest2 = pytester.path.joinpath("tests/subdir/conftest.py")
config.pluginmanager._importconftest( config.pluginmanager._importconftest(
conftest1, importmode="prepend", rootpath=pytester.path conftest1,
importmode="prepend",
rootpath=pytester.path,
consider_namespace_packages=False,
) )
ihook_a = session.gethookproxy(pytester.path / "tests") ihook_a = session.gethookproxy(pytester.path / "tests")
assert ihook_a is not None assert ihook_a is not None
config.pluginmanager._importconftest( config.pluginmanager._importconftest(
conftest2, importmode="prepend", rootpath=pytester.path conftest2,
importmode="prepend",
rootpath=pytester.path,
consider_namespace_packages=False,
) )
ihook_b = session.gethookproxy(pytester.path / "tests") ihook_b = session.gethookproxy(pytester.path / "tests")
assert ihook_a is not ihook_b assert ihook_a is not ihook_b
@ -398,7 +412,9 @@ class TestPytestPluginManager:
pytestpm: PytestPluginManager, pytestpm: PytestPluginManager,
) -> None: ) -> None:
mod = import_path( mod = import_path(
pytester.makepyfile("pytest_plugins='xyz'"), root=pytester.path pytester.makepyfile("pytest_plugins='xyz'"),
root=pytester.path,
consider_namespace_packages=False,
) )
with pytest.raises(ImportError): with pytest.raises(ImportError):
pytestpm.consider_conftest(mod, registration_name="unused") pytestpm.consider_conftest(mod, registration_name="unused")