Refactor code and improve docs

This commit is contained in:
Bruno Oliveira 2024-04-06 11:12:28 -03:00
parent f523279531
commit fddb7db189
2 changed files with 59 additions and 30 deletions

View File

@ -37,8 +37,6 @@ from typing import Union
import uuid import uuid
import warnings import warnings
from typing_extensions import TypeGuard
from _pytest.compat import assert_never from _pytest.compat import assert_never
from _pytest.outcomes import skip from _pytest.outcomes import skip
from _pytest.warning_types import PytestWarning from _pytest.warning_types import PytestWarning
@ -637,6 +635,7 @@ def _import_module_using_spec(
spec = importlib.util.spec_from_file_location(module_name, str(module_path)) spec = importlib.util.spec_from_file_location(module_name, str(module_path))
if spec_matches_module_path(spec, module_path): if spec_matches_module_path(spec, module_path):
assert spec is not None
mod = importlib.util.module_from_spec(spec) mod = importlib.util.module_from_spec(spec)
sys.modules[module_name] = mod sys.modules[module_name] = mod
spec.loader.exec_module(mod) # type: ignore[union-attr] spec.loader.exec_module(mod) # type: ignore[union-attr]
@ -649,7 +648,8 @@ def _import_module_using_spec(
def spec_matches_module_path( def spec_matches_module_path(
module_spec: Optional[ModuleSpec], module_path: Path module_spec: Optional[ModuleSpec], module_path: Path
) -> TypeGuard[ModuleSpec]: ) -> bool:
"""Return true if the given ModuleSpec can be used to import the given module path."""
if module_spec is not None and module_spec.origin is not None: if module_spec is not None and module_spec.origin is not None:
if Path(module_spec.origin) == module_path: if Path(module_spec.origin) == module_path:
return True return True
@ -775,7 +775,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_namespace_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: for namespace packages:
https://packaging.python.org/en/latest/guides/packaging-namespace-packages https://packaging.python.org/en/latest/guides/packaging-namespace-packages
@ -788,39 +788,48 @@ def resolve_pkg_root_and_module_name(
if consider_namespace_packages: if consider_namespace_packages:
start = path.parent if pkg_path is None else pkg_path.parent start = path.parent if pkg_path is None else pkg_path.parent
for candidate in (start, *start.parents): for candidate in (start, *start.parents):
if _is_importable(candidate, path): if is_importable(candidate, path):
# Point the pkg_root to the root of the namespace package. # Point the pkg_root to the root of the namespace package.
pkg_root = candidate.parent pkg_root = candidate
break break
if pkg_root is not None: if pkg_root is not None:
names = list(path.with_suffix("").relative_to(pkg_root).parts) module_name = compute_module_name(pkg_root, path)
if names[-1] == "__init__":
names.pop()
module_name = ".".join(names)
return pkg_root, module_name return pkg_root, module_name
raise CouldNotResolvePathError(f"Could not resolve for {path}") raise CouldNotResolvePathError(f"Could not resolve for {path}")
def _is_importable(root: Path, path: Path) -> bool: def is_importable(root: Path, module_path: Path) -> bool:
try: """
path_without_suffix = path.with_suffix("") Return if the given module path could be imported normally by Python, akin to the user
except ValueError: entering the REPL and importing the corresponding module name directly.
# Empty paths (such as Path.cwd()) might break meta_path hooks (like our own assertion rewriter). """
return False module_name = compute_module_name(root, module_path)
names = list(path_without_suffix.relative_to(root.parent).parts)
if names[-1] == "__init__":
names.pop()
module_name = ".".join(names)
try: try:
# Note this is different from what we do in ``import_path``, where we also search sys.meta_path.
# Searching sys.meta_path will eventually find a spec which can load the file even if the interpreter would
# not find this module normally in the REPL, which is exactly what we want to be able to do in
# ``import_path``, but not here.
spec = importlib.util.find_spec(module_name) spec = importlib.util.find_spec(module_name)
except (ImportError, ValueError, ImportWarning): except (ImportError, ValueError, ImportWarning):
return False return False
else: else:
return spec_matches_module_path(spec, path) return spec_matches_module_path(spec, module_path)
def compute_module_name(root: Path, path: Path) -> str:
"""Compute a module name based on a path and a root anchor."""
try:
path_without_suffix = path.with_suffix("")
except ValueError:
# Empty paths (such as Path.cwd()) might break meta_path hooks (like our own assertion rewriter).
return ""
names = list(path_without_suffix.relative_to(root).parts)
if names and names[-1] == "__init__":
names.pop()
return ".".join(names)
class CouldNotResolvePathError(Exception): class CouldNotResolvePathError(Exception):

View File

@ -18,9 +18,9 @@ from typing import Tuple
import unittest.mock import unittest.mock
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
from _pytest.pathlib import _is_importable
from _pytest.pathlib import bestrelpath from _pytest.pathlib import bestrelpath
from _pytest.pathlib import commonpath from _pytest.pathlib import commonpath
from _pytest.pathlib import compute_module_name
from _pytest.pathlib import CouldNotResolvePathError from _pytest.pathlib import CouldNotResolvePathError
from _pytest.pathlib import ensure_deletable from _pytest.pathlib import ensure_deletable
from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import fnmatch_ex
@ -30,6 +30,7 @@ from _pytest.pathlib import import_path
from _pytest.pathlib import ImportMode from _pytest.pathlib import ImportMode
from _pytest.pathlib import ImportPathMismatchError from _pytest.pathlib import ImportPathMismatchError
from _pytest.pathlib import insert_missing_modules from _pytest.pathlib import insert_missing_modules
from _pytest.pathlib import is_importable
from _pytest.pathlib import maybe_delete_a_numbered_dir from _pytest.pathlib import maybe_delete_a_numbered_dir
from _pytest.pathlib import module_name_from_path from _pytest.pathlib import module_name_from_path
from _pytest.pathlib import resolve_package_path from _pytest.pathlib import resolve_package_path
@ -1321,13 +1322,25 @@ class TestNamespacePackages:
pytester.syspathinsert() pytester.syspathinsert()
path = pytester.path / "bar.x" path = pytester.path / "bar.x"
path.mkdir() path.mkdir()
assert _is_importable(path.parent, path) is False assert is_importable(path.parent, path) is False
path = pytester.path / ".bar.x" path = pytester.path / ".bar.x"
path.mkdir() path.mkdir()
assert _is_importable(path.parent, path) is False assert is_importable(path.parent, path) is False
assert _is_importable(Path(), Path()) is False def test_compute_module_name(self, tmp_path: Path) -> None:
assert compute_module_name(tmp_path, tmp_path) == ""
assert compute_module_name(Path(), Path()) == ""
assert compute_module_name(tmp_path, tmp_path / "mod.py") == "mod"
assert compute_module_name(tmp_path, tmp_path / "src/app/bar") == "src.app.bar"
assert (
compute_module_name(tmp_path, tmp_path / "src/app/bar.py") == "src.app.bar"
)
assert (
compute_module_name(tmp_path, tmp_path / "src/app/bar/__init__.py")
== "src.app.bar"
)
@pytest.mark.parametrize("insert", [True, False]) @pytest.mark.parametrize("insert", [True, False])
def test_full_ns_packages_without_init_files( def test_full_ns_packages_without_init_files(
@ -1362,14 +1375,21 @@ class TestNamespacePackages:
def validate_namespace_package( def validate_namespace_package(
pytester: Pytester, paths: Sequence[Path], imports: Sequence[str] pytester: Pytester, paths: Sequence[Path], modules: Sequence[str]
) -> RunResult: ) -> RunResult:
"""Validate a Python namespace package by importing modules from it in a Python subprocess""" """
Validate that a Python namespace package is set up correctly.
In a sub interpreter, add 'paths' to sys.path and attempt to import the given modules.
In this module many tests configure a set of files as a namespace package, this function
is used as sanity check that our files are configured correctly from the point of view of Python.
"""
lines = [ lines = [
"import sys", "import sys",
# Configure sys.path. # Configure sys.path.
*[f"sys.path.append(r{str(x)!r})" for x in paths], *[f"sys.path.append(r{str(x)!r})" for x in paths],
# Imports. # Imports.
*[f"import {x}" for x in imports], *[f"import {x}" for x in modules],
] ]
return pytester.runpython_c("\n".join(lines)) return pytester.runpython_c("\n".join(lines))