Skip to content

Commit 504ceeb

Browse files
committed
Refine how we detect namespace packages
Previously we used a hand crafted approach to detect namespace packages, however we should rely on ``importlib`` to detect them for us. Fix #12112
1 parent 1fd8a10 commit 504ceeb

File tree

3 files changed

+34
-13
lines changed

3 files changed

+34
-13
lines changed

changelog/12112.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improve namespace packages detection when :confval:`consider_namespace_packages` is enabled, covering more situations (like editable installs).

doc/en/reference/reference.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1279,8 +1279,7 @@ passed multiple times. The expected format is ``name=value``. For example::
12791279
Controls if pytest should attempt to identify `namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages>`__
12801280
when collecting Python modules. Default is ``False``.
12811281

1282-
Set to ``True`` if you are testing namespace packages installed into a virtual environment and it is important for
1283-
your packages to be imported using their full namespace package name.
1282+
Set to ``True`` if the package you are testing is part of a namespace package.
12841283

12851284
Only `native namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#native-namespace-packages>`__
12861285
are supported, with no plans to support `legacy namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#legacy-namespace-packages>`__.

src/_pytest/pathlib.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -771,19 +771,11 @@ def resolve_pkg_root_and_module_name(
771771
pkg_path = resolve_package_path(path)
772772
if pkg_path is not None:
773773
pkg_root = pkg_path.parent
774-
# https://packaging.python.org/en/latest/guides/packaging-namespace-packages/
775774
if consider_namespace_packages:
776-
# Go upwards in the hierarchy, if we find a parent path included
777-
# in sys.path, it means the package found by resolve_package_path()
778-
# actually belongs to a namespace package.
779-
for parent in pkg_root.parents:
780-
# If any of the parent paths has a __init__.py, it means it is not
781-
# a namespace package (see the docs linked above).
782-
if (parent / "__init__.py").is_file():
783-
break
784-
if str(parent) in sys.path:
775+
for candidate in (pkg_root, *pkg_root.parents):
776+
if _is_namespace_package(candidate):
785777
# Point the pkg_root to the root of the namespace package.
786-
pkg_root = parent
778+
pkg_root = candidate.parent
787779
break
788780

789781
names = list(path.with_suffix("").relative_to(pkg_root).parts)
@@ -795,6 +787,35 @@ def resolve_pkg_root_and_module_name(
795787
raise CouldNotResolvePathError(f"Could not resolve for {path}")
796788

797789

790+
def _is_namespace_package(module_path: Path) -> bool:
791+
# If the path has na __init__.py file, it means it is not
792+
# a namespace package:.
793+
# https://packaging.python.org/en/latest/guides/packaging-namespace-packages.
794+
if (module_path / "__init__.py").is_file():
795+
return False
796+
797+
module_name = module_path.name
798+
799+
# Empty module names break find_spec.
800+
if not module_name:
801+
return False
802+
803+
# Modules starting with "." indicate relative imports and break find_spec, and we are only attempting
804+
# to find top-level namespace packages anyway.
805+
if module_name.startswith("."):
806+
return False
807+
808+
spec = importlib.util.find_spec(module_name)
809+
if spec is not None and spec.submodule_search_locations:
810+
# Found a spec, however make sure the module_path is in one of the search locations --
811+
# this ensures common module name like "src" (which might be in sys.path under different locations)
812+
# is only considered for the module_path we intend to.
813+
# Make sure to compare Path(s) instead of strings, this normalizes them on Windows.
814+
if module_path in [Path(x) for x in spec.submodule_search_locations]:
815+
return True
816+
return False
817+
818+
798819
class CouldNotResolvePathError(Exception):
799820
"""Custom exception raised by resolve_pkg_root_and_module_name."""
800821

0 commit comments

Comments
 (0)