Skip to content

Commit 1ca6423

Browse files
committed
Fix import_path for packages
For packages, `import_path` receives the path to the package's `__init__.py` file, however module names (as they live in `sys.modules`) should not include the `__init__` part. For example, `app/core/__init__.py` should be imported as `app.core`, not as `app.core.__init__`. Fix #11306
1 parent 9c8937b commit 1ca6423

File tree

3 files changed

+50
-0
lines changed

3 files changed

+50
-0
lines changed

changelog/11306.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed bug using ``--importmode=importlib`` which would cause package ``__init__.py`` files to be imported more than once in some cases.

src/_pytest/pathlib.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,10 @@ def module_name_from_path(path: Path, root: Path) -> str:
623623
# Use the parts for the relative path to the root path.
624624
path_parts = relative_path.parts
625625

626+
# Module name for packages do not contain the __init__ file.
627+
if path_parts[-1] == "__init__":
628+
path_parts = path_parts[:-1]
629+
626630
return ".".join(path_parts)
627631

628632

testing/test_pathlib.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from _pytest.pathlib import get_extended_length_path_str
1919
from _pytest.pathlib import get_lock_path
2020
from _pytest.pathlib import import_path
21+
from _pytest.pathlib import ImportMode
2122
from _pytest.pathlib import ImportPathMismatchError
2223
from _pytest.pathlib import insert_missing_modules
2324
from _pytest.pathlib import maybe_delete_a_numbered_dir
@@ -585,6 +586,10 @@ def test_module_name_from_path(self, tmp_path: Path) -> None:
585586
result = module_name_from_path(Path("/home/foo/test_foo.py"), Path("/bar"))
586587
assert result == "home.foo.test_foo"
587588

589+
# Importing __init__.py files should return the package as module name.
590+
result = module_name_from_path(tmp_path / "src/app/__init__.py", tmp_path)
591+
assert result == "src.app"
592+
588593
def test_insert_missing_modules(
589594
self, monkeypatch: MonkeyPatch, tmp_path: Path
590595
) -> None:
@@ -615,3 +620,43 @@ def test_parent_contains_child_module_attribute(
615620
assert sorted(modules) == ["xxx", "xxx.tests", "xxx.tests.foo"]
616621
assert modules["xxx"].tests is modules["xxx.tests"]
617622
assert modules["xxx.tests"].foo is modules["xxx.tests.foo"]
623+
624+
def test_importlib_package(self, monkeypatch: MonkeyPatch, tmp_path: Path):
625+
"""
626+
Importing a package using --importmode=importlib should not import the
627+
package's __init__.py file more than once (#11306).
628+
"""
629+
monkeypatch.chdir(tmp_path)
630+
monkeypatch.syspath_prepend(tmp_path)
631+
632+
tmp_path.joinpath("app").mkdir()
633+
init = tmp_path.joinpath("app/__init__.py")
634+
init.write_text(
635+
dedent(
636+
"""
637+
from .singleton import Singleton
638+
639+
instance = Singleton()
640+
"""
641+
),
642+
encoding="ascii",
643+
)
644+
singleton = tmp_path.joinpath("app/singleton.py")
645+
singleton.write_text(
646+
dedent(
647+
"""
648+
class Singleton:
649+
BOOM = []
650+
651+
def __init__(self) -> None:
652+
cls = type(self)
653+
cls.BOOM.append(True)
654+
if len(cls.BOOM) > 1:
655+
raise RuntimeError("Already initialized")
656+
"""
657+
),
658+
encoding="ascii",
659+
)
660+
661+
mod = import_path(init, root=tmp_path, mode=ImportMode.importlib)
662+
assert mod.instance.BOOM == [True]

0 commit comments

Comments
 (0)