Skip to content

Commit 111c0d9

Browse files
committed
Add consider_namespace_packages ini option
Fix pytest-dev#11475
1 parent 199d4e2 commit 111c0d9

File tree

10 files changed

+315
-77
lines changed

10 files changed

+315
-77
lines changed

src/_pytest/config/__init__.py

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,8 @@ def _set_initial_conftests(
547547
confcutdir: Optional[Path],
548548
invocation_dir: Path,
549549
importmode: Union[ImportMode, str],
550+
*,
551+
consider_namespace_packages: bool,
550552
) -> None:
551553
"""Load initial conftest files given a preparsed "namespace".
552554
@@ -572,10 +574,20 @@ def _set_initial_conftests(
572574
# Ensure we do not break if what appears to be an anchor
573575
# is in fact a very long option (#10169, #11394).
574576
if safe_exists(anchor):
575-
self._try_load_conftest(anchor, importmode, rootpath)
577+
self._try_load_conftest(
578+
anchor,
579+
importmode,
580+
rootpath,
581+
consider_namespace_packages=consider_namespace_packages,
582+
)
576583
foundanchor = True
577584
if not foundanchor:
578-
self._try_load_conftest(invocation_dir, importmode, rootpath)
585+
self._try_load_conftest(
586+
invocation_dir,
587+
importmode,
588+
rootpath,
589+
consider_namespace_packages=consider_namespace_packages,
590+
)
579591

580592
def _is_in_confcutdir(self, path: Path) -> bool:
581593
"""Whether to consider the given path to load conftests from."""
@@ -593,17 +605,37 @@ def _is_in_confcutdir(self, path: Path) -> bool:
593605
return path not in self._confcutdir.parents
594606

595607
def _try_load_conftest(
596-
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
608+
self,
609+
anchor: Path,
610+
importmode: Union[str, ImportMode],
611+
rootpath: Path,
612+
*,
613+
consider_namespace_packages: bool,
597614
) -> None:
598-
self._loadconftestmodules(anchor, importmode, rootpath)
615+
self._loadconftestmodules(
616+
anchor,
617+
importmode,
618+
rootpath,
619+
consider_namespace_packages=consider_namespace_packages,
620+
)
599621
# let's also consider test* subdirs
600622
if anchor.is_dir():
601623
for x in anchor.glob("test*"):
602624
if x.is_dir():
603-
self._loadconftestmodules(x, importmode, rootpath)
625+
self._loadconftestmodules(
626+
x,
627+
importmode,
628+
rootpath,
629+
consider_namespace_packages=consider_namespace_packages,
630+
)
604631

605632
def _loadconftestmodules(
606-
self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
633+
self,
634+
path: Path,
635+
importmode: Union[str, ImportMode],
636+
rootpath: Path,
637+
*,
638+
consider_namespace_packages: bool,
607639
) -> None:
608640
if self._noconftest:
609641
return
@@ -620,7 +652,12 @@ def _loadconftestmodules(
620652
if self._is_in_confcutdir(parent):
621653
conftestpath = parent / "conftest.py"
622654
if conftestpath.is_file():
623-
mod = self._importconftest(conftestpath, importmode, rootpath)
655+
mod = self._importconftest(
656+
conftestpath,
657+
importmode,
658+
rootpath,
659+
consider_namespace_packages=consider_namespace_packages,
660+
)
624661
clist.append(mod)
625662
self._dirpath2confmods[directory] = clist
626663

@@ -642,7 +679,12 @@ def _rget_with_confmod(
642679
raise KeyError(name)
643680

644681
def _importconftest(
645-
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
682+
self,
683+
conftestpath: Path,
684+
importmode: Union[str, ImportMode],
685+
rootpath: Path,
686+
*,
687+
consider_namespace_packages: bool,
646688
) -> types.ModuleType:
647689
conftestpath_plugin_name = str(conftestpath)
648690
existing = self.get_plugin(conftestpath_plugin_name)
@@ -661,7 +703,12 @@ def _importconftest(
661703
pass
662704

663705
try:
664-
mod = import_path(conftestpath, mode=importmode, root=rootpath)
706+
mod = import_path(
707+
conftestpath,
708+
mode=importmode,
709+
root=rootpath,
710+
consider_namespace_packages=consider_namespace_packages,
711+
)
665712
except Exception as e:
666713
assert e.__traceback__ is not None
667714
raise ConftestImportFailure(conftestpath, cause=e) from e
@@ -1177,6 +1224,9 @@ def pytest_load_initial_conftests(self, early_config: "Config") -> None:
11771224
confcutdir=early_config.known_args_namespace.confcutdir,
11781225
invocation_dir=early_config.invocation_params.dir,
11791226
importmode=early_config.known_args_namespace.importmode,
1227+
consider_namespace_packages=early_config.getini(
1228+
"consider_namespace_packages"
1229+
),
11801230
)
11811231

11821232
def _initini(self, args: Sequence[str]) -> None:

src/_pytest/main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,12 @@ def pytest_addoption(parser: Parser) -> None:
222222
help="Prepend/append to sys.path when importing test modules and conftest "
223223
"files. Default: prepend.",
224224
)
225+
parser.addini(
226+
"consider_namespace_packages",
227+
type="bool",
228+
default=False,
229+
help="Consider namespace packages when resolving module names during import",
230+
)
225231

226232
group = parser.getgroup("debugconfig", "test session debugging and configuration")
227233
group.addoption(

src/_pytest/pathlib.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,7 @@ def import_path(
488488
*,
489489
mode: Union[str, ImportMode] = ImportMode.prepend,
490490
root: Path,
491+
consider_namespace_packages: bool,
491492
) -> ModuleType:
492493
"""
493494
Import and return a module from the given path, which can be a file (a module) or
@@ -515,6 +516,9 @@ def import_path(
515516
a unique name for the module being imported so it can safely be stored
516517
into ``sys.modules``.
517518
519+
:param consider_namespace_packages:
520+
If True, consider namespace packages when resolving module names.
521+
518522
:raises ImportPathMismatchError:
519523
If after importing the given `path` and the module `__file__`
520524
are different. Only raised in `prepend` and `append` modes.
@@ -530,7 +534,7 @@ def import_path(
530534
# without touching sys.path.
531535
try:
532536
pkg_root, module_name = resolve_pkg_root_and_module_name(
533-
path, consider_ns_packages=True
537+
path, consider_namespace_packages=consider_namespace_packages
534538
)
535539
except CouldNotResolvePathError:
536540
pass
@@ -556,7 +560,7 @@ def import_path(
556560

557561
try:
558562
pkg_root, module_name = resolve_pkg_root_and_module_name(
559-
path, consider_ns_packages=True
563+
path, consider_namespace_packages=consider_namespace_packages
560564
)
561565
except CouldNotResolvePathError:
562566
pkg_root, module_name = path.parent, path.stem
@@ -674,7 +678,7 @@ def module_name_from_path(path: Path, root: Path) -> str:
674678
# Module names cannot contain ".", normalize them to "_". This prevents
675679
# a directory having a "." in the name (".env.310" for example) causing extra intermediate modules.
676680
# Also, important to replace "." at the start of paths, as those are considered relative imports.
677-
path_parts = [x.replace(".", "_") for x in path_parts]
681+
path_parts = tuple(x.replace(".", "_") for x in path_parts)
678682

679683
return ".".join(path_parts)
680684

@@ -738,7 +742,7 @@ def resolve_package_path(path: Path) -> Optional[Path]:
738742

739743

740744
def resolve_pkg_root_and_module_name(
741-
path: Path, *, consider_ns_packages: bool = False
745+
path: Path, *, consider_namespace_packages: bool = False
742746
) -> Tuple[Path, str]:
743747
"""
744748
Return the path to the directory of the root package that contains the
@@ -753,7 +757,7 @@ def resolve_pkg_root_and_module_name(
753757
754758
Passing the full path to `models.py` will yield Path("src") and "app.core.models".
755759
756-
If consider_ns_packages is True, then we additionally check upwards in the hierarchy
760+
If consider_namespace_packages is True, then we additionally check upwards in the hierarchy
757761
until we find a directory that is reachable from sys.path, which marks it as a namespace package:
758762
759763
https://packaging.python.org/en/latest/guides/packaging-namespace-packages
@@ -764,7 +768,7 @@ def resolve_pkg_root_and_module_name(
764768
if pkg_path is not None:
765769
pkg_root = pkg_path.parent
766770
# https://packaging.python.org/en/latest/guides/packaging-namespace-packages/
767-
if consider_ns_packages:
771+
if consider_namespace_packages:
768772
# Go upwards in the hierarchy, if we find a parent path included
769773
# in sys.path, it means the package found by resolve_package_path()
770774
# actually belongs to a namespace package.

src/_pytest/python.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,7 +516,12 @@ def importtestmodule(
516516
# We assume we are only called once per module.
517517
importmode = config.getoption("--import-mode")
518518
try:
519-
mod = import_path(path, mode=importmode, root=config.rootpath)
519+
mod = import_path(
520+
path,
521+
mode=importmode,
522+
root=config.rootpath,
523+
consider_namespace_packages=config.getini("consider_namespace_packages"),
524+
)
520525
except SyntaxError as e:
521526
raise nodes.Collector.CollectError(
522527
ExceptionInfo.from_current().getrepr(style="short")

src/_pytest/runner.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,9 @@ def collect() -> List[Union[Item, Collector]]:
380380
collector.path,
381381
collector.config.getoption("importmode"),
382382
rootpath=collector.config.rootpath,
383+
consider_namespace_packages=collector.config.getini(
384+
"consider_namespace_packages"
385+
),
383386
)
384387

385388
return list(collector.collect())

testing/code/test_excinfo.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def test_traceback_cut(self) -> None:
180180
def test_traceback_cut_excludepath(self, pytester: Pytester) -> None:
181181
p = pytester.makepyfile("def f(): raise ValueError")
182182
with pytest.raises(ValueError) as excinfo:
183-
import_path(p, root=pytester.path).f() # type: ignore[attr-defined]
183+
import_path(p, root=pytester.path, consider_namespace_packages=False).f() # type: ignore[attr-defined]
184184
basedir = Path(pytest.__file__).parent
185185
newtraceback = excinfo.traceback.cut(excludepath=basedir)
186186
for x in newtraceback:
@@ -543,7 +543,9 @@ def importasmod(source):
543543
tmp_path.joinpath("__init__.py").touch()
544544
modpath.write_text(source, encoding="utf-8")
545545
importlib.invalidate_caches()
546-
return import_path(modpath, root=tmp_path)
546+
return import_path(
547+
modpath, root=tmp_path, consider_namespace_packages=False
548+
)
547549

548550
return importasmod
549551

testing/code/test_source.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ def method(self):
296296
)
297297
path = tmp_path.joinpath("a.py")
298298
path.write_text(str(source), encoding="utf-8")
299-
mod: Any = import_path(path, root=tmp_path)
299+
mod: Any = import_path(path, root=tmp_path, consider_namespace_packages=False)
300300
s2 = Source(mod.A)
301301
assert str(source).strip() == str(s2).strip()
302302

0 commit comments

Comments
 (0)