Skip to content

Commit

Permalink
Refine how we detect namespace packages
Browse files Browse the repository at this point in the history
Previously we used a hand crafted approach to detect namespace packages, however we should rely on ``importlib`` to detect them for us.

Fix pytest-dev#12112
  • Loading branch information
nicoddemus committed Mar 30, 2024
1 parent 12e061e commit 0d4a4f3
Show file tree
Hide file tree
Showing 2 changed files with 28 additions and 7 deletions.
1 change: 1 addition & 0 deletions changelog/12112.bugfix.rst
@@ -0,0 +1 @@
Refine how namespace packages are detected when :confval:`consider_namespace_packages` is enabled.
34 changes: 27 additions & 7 deletions src/_pytest/pathlib.py
Expand Up @@ -773,17 +773,14 @@ def resolve_pkg_root_and_module_name(
pkg_root = pkg_path.parent
# https://packaging.python.org/en/latest/guides/packaging-namespace-packages/
if consider_namespace_packages:
# Go upwards in the hierarchy, if we find a parent path included
# in sys.path, it means the package found by resolve_package_path()
# actually belongs to a namespace package.
for parent in pkg_root.parents:
for candidate in (pkg_root, *pkg_root.parents):
# If any of the parent paths has a __init__.py, it means it is not
# a namespace package (see the docs linked above).
if (parent / "__init__.py").is_file():
if (candidate / "__init__.py").is_file():
break
if str(parent) in sys.path:
if _is_namespace_package(candidate):
# Point the pkg_root to the root of the namespace package.
pkg_root = parent
pkg_root = candidate.parent
break

names = list(path.with_suffix("").relative_to(pkg_root).parts)
Expand All @@ -795,6 +792,29 @@ def resolve_pkg_root_and_module_name(
raise CouldNotResolvePathError(f"Could not resolve for {path}")


def _is_namespace_package(module_path: Path) -> bool:
module_name = module_path.name

# Empty module names break find_spec.
if not module_name:
return False

# Modules starting with "." indicate relative imports and break find_spec, and we are only attempting
# to find top-level namespace packages anyway.
if module_name.startswith("."):
return False

spec = importlib.util.find_spec(module_name)
if spec is not None and spec.submodule_search_locations:
# Found a spec, however make sure the module_path is in one of the search locations --
# this ensures common module name like "src" (which might be in sys.path under different locations)
# is only considered for the module_path we intend to.
# Make sure to compare Path(s) instead of strings, as this will normalize them on Windows.
if module_path in [Path(x) for x in spec.submodule_search_locations]:
return True
return False


class CouldNotResolvePathError(Exception):
"""Custom exception raised by resolve_pkg_root_and_module_name."""

Expand Down

0 comments on commit 0d4a4f3

Please sign in to comment.