Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
nicoddemus committed Feb 17, 2024
1 parent 9edaa9e commit e054ca6
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 30 deletions.
72 changes: 44 additions & 28 deletions src/_pytest/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,32 +522,28 @@ def import_path(
raise ImportError(path)

if mode is ImportMode.importlib:
pkg_path = resolve_package_path(path)
if pkg_path is not None:
pkg_root = pkg_path.parent
names = list(path.with_suffix("").relative_to(pkg_root).parts)
if names[-1] == "__init__":
names.pop()
module_name = ".".join(names)
else:
pkg_root = path.parent
module_name = path.stem

print("IMPORTMODE")
print(f" {path=}")
# Obtain the canonical name of this path if we can resolve it to a python package,
# and try to import it without changing sys.path.
# If it works, we import it and return the module.
_, module_name = resolve_pkg_root_and_module_name(path)
try:
importlib.import_module(module_name)
except (ImportError, ImportWarning) as e:
print(f" FAILED: {e}")
except (ImportError, ImportWarning):

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

View check run for this annotation

Codecov / codecov/patch

src/_pytest/pathlib.py#L528-L531

Added lines #L528 - L531 were not covered by tests
# Cannot be imported normally with the current sys.path, so fallback
# to the more complex import-path mechanism.
pass

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

View check run for this annotation

Codecov / codecov/patch

src/_pytest/pathlib.py#L534

Added line #L534 was not covered by tests
else:
print(f" WORKED: {module_name}")
# Double check that the imported module is the one we
# were given, otherwise it is easy to import modules with common names like "test"
# from another location.
mod = sys.modules[module_name]

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

View check run for this annotation

Codecov / codecov/patch

src/_pytest/pathlib.py#L539

Added line #L539 was not covered by tests
if mod.__file__ and Path(mod.__file__) == path:
return mod

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

View check run for this annotation

Codecov / codecov/patch

src/_pytest/pathlib.py#L541

Added line #L541 was not covered by tests

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

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

pkg_path = resolve_package_path(path)
if pkg_path is not None:
pkg_root = pkg_path.parent
names = list(path.with_suffix("").relative_to(pkg_root).parts)
if names[-1] == "__init__":
names.pop()
module_name = ".".join(names)
else:
pkg_root = path.parent
module_name = path.stem
pkg_root, module_name = resolve_pkg_root_and_module_name(path)

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


def resolve_pkg_root_and_module_name(path: Path) -> Tuple[Path, str]:
"""
Return the path to the directory of the root package that contains the
given Python file, and its module name:
src/
app/
__init__.py
core/
__init__.py
models.py
Passing the full path to `models.py` will yield Path("src") and "app.core.models".
If the given path does not belong to a package (missing __init__.py) files, returns
just the parent path and the name of the file as module name.
"""
pkg_path = resolve_package_path(path)
if pkg_path is not None:
pkg_root = pkg_path.parent
names = list(path.with_suffix("").relative_to(pkg_root).parts)
if names[-1] == "__init__":
names.pop()
module_name = ".".join(names)
else:
pkg_root = path.parent
module_name = path.stem
return pkg_root, module_name


def scandir(
path: Union[str, "os.PathLike[str]"],
sort_key: Callable[["os.DirEntry[str]"], object] = lambda entry: entry.name,
Expand Down
32 changes: 30 additions & 2 deletions testing/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from _pytest.pathlib import maybe_delete_a_numbered_dir
from _pytest.pathlib import module_name_from_path
from _pytest.pathlib import resolve_package_path
from _pytest.pathlib import resolve_pkg_root_and_module_name
from _pytest.pathlib import safe_exists
from _pytest.pathlib import symlink_or_skip
from _pytest.pathlib import visit
Expand Down Expand Up @@ -598,6 +599,29 @@ def test_module_name_from_path(self, tmp_path: Path) -> None:
result = module_name_from_path(tmp_path / "__init__.py", tmp_path)
assert result == "__init__"

# Modules which start with "." are considered relative and will not be imported
# unless part of a package, so we replace it with a "_" when generating the fake module name.
result = module_name_from_path(tmp_path / ".env/tests/test_foo.py", tmp_path)
assert result == "_env.tests.test_foo"

def test_resolve_pkg_root_and_module_name(self, tmp_path: Path) -> None:
# Create a directory structure first without __init__.py files.
(tmp_path / "src/app/core").mkdir(parents=True)
models_py = tmp_path / "src/app/core/models.py"
models_py.touch()
assert resolve_pkg_root_and_module_name(models_py) == (
models_py.parent,
"models",
)

# Create the __init__.py files, it should now resolve to a proper module name.
(tmp_path / "src/app/__init__.py").touch()
(tmp_path / "src/app/core/__init__.py").touch()
assert resolve_pkg_root_and_module_name(models_py) == (
tmp_path / "src",
"app.core.models",
)

def test_insert_missing_modules(
self, monkeypatch: MonkeyPatch, tmp_path: Path
) -> None:
Expand Down Expand Up @@ -734,14 +758,18 @@ def foo():
2
'''
"""
)
),
encoding="ascii",
)

monkeypatch.syspath_prepend(app.parent)

test_path = path / ".tests/test_core.py"
test_path.parent.mkdir(parents=True)
test_path.write_text("import app.core\n\ndef test(): pass")
test_path.write_text(
"import app.core\n\ndef test(): pass",
encoding="ascii",
)

def test_import_using_normal_mechanism_first(
self, monkeypatch: MonkeyPatch, pytester: Pytester
Expand Down

0 comments on commit e054ca6

Please sign in to comment.