Skip to content

Commit 9dbb819

Browse files
committed
WIP
1 parent 5b92d46 commit 9dbb819

File tree

2 files changed

+74
-30
lines changed

2 files changed

+74
-30
lines changed

src/_pytest/pathlib.py

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -522,32 +522,28 @@ def import_path(
522522
raise ImportError(path)
523523

524524
if mode is ImportMode.importlib:
525-
pkg_path = resolve_package_path(path)
526-
if pkg_path is not None:
527-
pkg_root = pkg_path.parent
528-
names = list(path.with_suffix("").relative_to(pkg_root).parts)
529-
if names[-1] == "__init__":
530-
names.pop()
531-
module_name = ".".join(names)
532-
else:
533-
pkg_root = path.parent
534-
module_name = path.stem
535-
536-
print("IMPORTMODE")
537-
print(f" {path=}")
525+
# Obtain the canonical name of this path if we can resolve it to a python package,
526+
# and try to import it without changing sys.path.
527+
# If it works, we import it and return the module.
528+
_, module_name = resolve_pkg_root_and_module_name(path)
538529
try:
539530
importlib.import_module(module_name)
540-
except (ImportError, ImportWarning) as e:
541-
print(f" FAILED: {e}")
531+
except (ImportError, ImportWarning):
532+
# Cannot be imported normally with the current sys.path, so fallback
533+
# to the more complex import-path mechanism.
534+
pass
542535
else:
543-
print(f" WORKED: {module_name}")
536+
# Double check that the imported module is the one we
537+
# were given, otherwise it is easy to import modules with common names like "test"
538+
# from another location.
544539
mod = sys.modules[module_name]
545540
if mod.__file__ and Path(mod.__file__) == path:
546541
return mod
547542

543+
# Could not import the module with the current sys.path, so we fall back
544+
# to importing the file as a standalone module, not being a part of a package.
548545
module_name = module_name_from_path(path, root)
549546
with contextlib.suppress(KeyError):
550-
print(f" CACHED {module_name=}")
551547
return sys.modules[module_name]
552548

553549
for meta_importer in sys.meta_path:
@@ -563,19 +559,9 @@ def import_path(
563559
sys.modules[module_name] = mod
564560
spec.loader.exec_module(mod) # type: ignore[union-attr]
565561
insert_missing_modules(sys.modules, module_name)
566-
print(f" IMPORTED_AS {module_name=}")
567562
return mod
568563

569-
pkg_path = resolve_package_path(path)
570-
if pkg_path is not None:
571-
pkg_root = pkg_path.parent
572-
names = list(path.with_suffix("").relative_to(pkg_root).parts)
573-
if names[-1] == "__init__":
574-
names.pop()
575-
module_name = ".".join(names)
576-
else:
577-
pkg_root = path.parent
578-
module_name = path.stem
564+
pkg_root, module_name = resolve_pkg_root_and_module_name(path)
579565

580566
# Change sys.path permanently: restoring it at the end of this function would cause surprising
581567
# problems because of delayed imports: for example, a conftest.py file imported by this function
@@ -717,6 +703,36 @@ def resolve_package_path(path: Path) -> Optional[Path]:
717703
return result
718704

719705

706+
def resolve_pkg_root_and_module_name(path: Path) -> Tuple[Path, str]:
707+
"""
708+
Return the path to the directory of the root package that contains the
709+
given Python file, and its module name:
710+
711+
src/
712+
app/
713+
__init__.py
714+
core/
715+
__init__.py
716+
models.py
717+
718+
Passing the full path to `models.py` will yield Path("src") and "app.core.models".
719+
720+
If the given path does not belong to a package (missing __init__.py) files, returns
721+
just the parent path and the name of the file as module name.
722+
"""
723+
pkg_path = resolve_package_path(path)
724+
if pkg_path is not None:
725+
pkg_root = pkg_path.parent
726+
names = list(path.with_suffix("").relative_to(pkg_root).parts)
727+
if names[-1] == "__init__":
728+
names.pop()
729+
module_name = ".".join(names)
730+
else:
731+
pkg_root = path.parent
732+
module_name = path.stem
733+
return pkg_root, module_name
734+
735+
720736
def scandir(
721737
path: Union[str, "os.PathLike[str]"],
722738
sort_key: Callable[["os.DirEntry[str]"], object] = lambda entry: entry.name,

testing/test_pathlib.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from _pytest.pathlib import maybe_delete_a_numbered_dir
2626
from _pytest.pathlib import module_name_from_path
2727
from _pytest.pathlib import resolve_package_path
28+
from _pytest.pathlib import resolve_pkg_root_and_module_name
2829
from _pytest.pathlib import safe_exists
2930
from _pytest.pathlib import symlink_or_skip
3031
from _pytest.pathlib import visit
@@ -598,6 +599,29 @@ def test_module_name_from_path(self, tmp_path: Path) -> None:
598599
result = module_name_from_path(tmp_path / "__init__.py", tmp_path)
599600
assert result == "__init__"
600601

602+
# Modules which start with "." are considered relative and will not be imported
603+
# unless part of a package, so we replace it with a "_" when generating the fake module name.
604+
result = module_name_from_path(tmp_path / ".env/tests/test_foo.py", tmp_path)
605+
assert result == "_env.tests.test_foo"
606+
607+
def test_resolve_pkg_root_and_module_name(self, tmp_path: Path) -> None:
608+
# Create a directory structure first without __init__.py files.
609+
(tmp_path / "src/app/core").mkdir(parents=True)
610+
models_py = tmp_path / "src/app/core/models.py"
611+
models_py.touch()
612+
assert resolve_pkg_root_and_module_name(models_py) == (
613+
models_py.parent,
614+
"models",
615+
)
616+
617+
# Create the __init__.py files, it should now resolve to a proper module name.
618+
(tmp_path / "src/app/__init__.py").touch()
619+
(tmp_path / "src/app/core/__init__.py").touch()
620+
assert resolve_pkg_root_and_module_name(models_py) == (
621+
tmp_path / "src",
622+
"app.core.models",
623+
)
624+
601625
def test_insert_missing_modules(
602626
self, monkeypatch: MonkeyPatch, tmp_path: Path
603627
) -> None:
@@ -734,14 +758,18 @@ def foo():
734758
2
735759
'''
736760
"""
737-
)
761+
),
762+
encoding="ascii",
738763
)
739764

740765
monkeypatch.syspath_prepend(app.parent)
741766

742767
test_path = path / ".tests/test_core.py"
743768
test_path.parent.mkdir(parents=True)
744-
test_path.write_text("import app.core\n\ndef test(): pass")
769+
test_path.write_text(
770+
"import app.core\n\ndef test(): pass",
771+
encoding="ascii",
772+
)
745773

746774
def test_import_using_normal_mechanism_first(
747775
self, monkeypatch: MonkeyPatch, pytester: Pytester

0 commit comments

Comments
 (0)