Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use shortest module name for importlib imports #11931

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ Pavel Karateev
Paweł Adamczak
Pedro Algarvio
Petter Strandmark
Philipp A.
Philipp Loose
Pierre Sassoulas
Pieter Mulder
Expand Down
1 change: 1 addition & 0 deletions changelog/11931.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix some instances of importing doctests’ parent modules when using `--import-mode=importlib`.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ ignore = [
"PLW2901", # for loop variable overwritten by assignment target
"PLR5501", # Use `elif` instead of `else` then `if`
]
allowed-confusables = ["’"]

[tool.ruff.lint.pycodestyle]
# In order to be able to format for 88 char in ruff format
Expand Down
28 changes: 18 additions & 10 deletions src/_pytest/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,21 +607,18 @@

def module_name_from_path(path: Path, root: Path) -> str:
"""
Return a dotted module name based on the given path, anchored on root.
Return a dotted module name based on the given path,
anchored on root or the most likely entry in `sys.path`.

For example: path="projects/src/tests/test_foo.py" and root="/projects", the
resulting module name will be "src.tests.test_foo".
"""
path = path.with_suffix("")
try:
relative_path = path.relative_to(root)
except ValueError:
# If we can't get a relative path to root, use the full path, except
# for the first part ("d:\\" or "/" depending on the platform, for example).
path_parts = path.parts[1:]
else:
# Use the parts for the relative path to the root path.
path_parts = relative_path.parts
candidates = (
_maybe_relative_parts(path, dir)
for dir in itertools.chain([root], map(Path, sys.path))
)
path_parts = min(candidates, key=len) # type: ignore[arg-type]

Check warning on line 621 in src/_pytest/pathlib.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/pathlib.py#L621

Added line #L621 was not covered by tests

# Module name for packages do not contain the __init__ file, unless
# the `__init__.py` file is at the root.
Expand All @@ -631,6 +628,17 @@
return ".".join(path_parts)


def _maybe_relative_parts(path: Path, root: Path) -> "tuple[str, ...]":
try:
relative_path = path.relative_to(root)
except ValueError:

Check warning on line 634 in src/_pytest/pathlib.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/pathlib.py#L632-L634

Added lines #L632 - L634 were not covered by tests
# If we can't get a relative path to root, use the full path, except
# for the first part ("d:\\" or "/" depending on the platform, for example).
return path.parts[1:]

Check warning on line 637 in src/_pytest/pathlib.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/pathlib.py#L637

Added line #L637 was not covered by tests
# Use the parts for the relative path to the root path.
return relative_path.parts

Check warning on line 639 in src/_pytest/pathlib.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/pathlib.py#L639

Added line #L639 was not covered by tests


def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) -> None:
"""
Used by ``import_path`` to create intermediate modules when using mode=importlib.
Expand Down
28 changes: 28 additions & 0 deletions testing/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,34 @@ def __init__(self) -> None:
mod = import_path(init, root=tmp_path, mode=ImportMode.importlib)
assert len(mod.instance.INSTANCES) == 1

def test_importlib_doctest(self, monkeypatch: MonkeyPatch, tmp_path: Path) -> None:
"""
Importing a package using --importmode=importlib should
import the package using the canonical name
"""
proj_dir = tmp_path / "proj"
proj_dir.mkdir()
pkgs_dir = tmp_path / "pkgs"
pkgs_dir.mkdir()
monkeypatch.chdir(proj_dir)
monkeypatch.syspath_prepend(pkgs_dir)
# this is also there, but shouldn’t be imported from
monkeypatch.syspath_prepend(proj_dir)

package_name = "importlib_doctest"
# pkgs_dir is second to set `init`
for directory in [proj_dir / "src", pkgs_dir]:
pkgdir = directory / package_name
pkgdir.mkdir(parents=True)
init = pkgdir / "__init__.py"
init.write_text("", encoding="ascii")

# the PyTest root is `proj_dir`, but the package is imported from `pkgs_dir`
mod = import_path(init, root=proj_dir, mode=ImportMode.importlib)
# assert that it’s imported with the canonical name, not “path.to.package.<name>”
mod_names = [n for n, m in sys.modules.items() if m is mod]
assert mod_names == ["importlib_doctest"]

def test_importlib_root_is_package(self, pytester: Pytester) -> None:
"""
Regression for importing a `__init__`.py file that is at the root
Expand Down
Loading