Skip to content

Commit

Permalink
Fix import_path for packages
Browse files Browse the repository at this point in the history
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 pytest-dev#11306
  • Loading branch information
nicoddemus committed Sep 5, 2023
1 parent 9c8937b commit 38a9af2
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 0 deletions.
1 change: 1 addition & 0 deletions changelog/11306.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed bug using ``--importmode=importlib`` which would cause package ``__init__.py`` files to be imported more than once in some cases.
4 changes: 4 additions & 0 deletions src/_pytest/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,10 @@ def module_name_from_path(path: Path, root: Path) -> str:
# Use the parts for the relative path to the root path.
path_parts = relative_path.parts

# Module name for packages do not contain the __init__ file.
if path_parts[-1] == "__init__":
path_parts = path_parts[:-1]

return ".".join(path_parts)


Expand Down
46 changes: 46 additions & 0 deletions testing/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from _pytest.pathlib import get_extended_length_path_str
from _pytest.pathlib import get_lock_path
from _pytest.pathlib import import_path
from _pytest.pathlib import ImportMode
from _pytest.pathlib import ImportPathMismatchError
from _pytest.pathlib import insert_missing_modules
from _pytest.pathlib import maybe_delete_a_numbered_dir
Expand Down Expand Up @@ -585,6 +586,10 @@ def test_module_name_from_path(self, tmp_path: Path) -> None:
result = module_name_from_path(Path("/home/foo/test_foo.py"), Path("/bar"))
assert result == "home.foo.test_foo"

# Importing __init__.py files should return the package as module name.
result = module_name_from_path(tmp_path / "src/app/__init__.py", tmp_path)
assert result == "src.app"

def test_insert_missing_modules(
self, monkeypatch: MonkeyPatch, tmp_path: Path
) -> None:
Expand Down Expand Up @@ -615,3 +620,44 @@ def test_parent_contains_child_module_attribute(
assert sorted(modules) == ["xxx", "xxx.tests", "xxx.tests.foo"]
assert modules["xxx"].tests is modules["xxx.tests"]
assert modules["xxx.tests"].foo is modules["xxx.tests.foo"]

def test_importlib_package(self, monkeypatch: MonkeyPatch, tmp_path: Path):
"""
Importing a package using --importmode=importlib should not import the
package's __init__.py file more than once (#11306).
"""
monkeypatch.chdir(tmp_path)
monkeypatch.syspath_prepend(tmp_path)

package_name = "importlib_import_package"
tmp_path.joinpath(package_name).mkdir()
init = tmp_path.joinpath(f"{package_name}/__init__.py")
init.write_text(
dedent(
"""
from .singleton import Singleton
instance = Singleton()
"""
),
encoding="ascii",
)
singleton = tmp_path.joinpath(f"{package_name}/singleton.py")
singleton.write_text(
dedent(
"""
class Singleton:
BOOM = []
def __init__(self) -> None:
cls = type(self)
cls.BOOM.append(True)
if len(cls.BOOM) > 1:
raise RuntimeError("Already initialized")
"""
),
encoding="ascii",
)

mod = import_path(init, root=tmp_path, mode=ImportMode.importlib)
assert mod.instance.BOOM == [True]

0 comments on commit 38a9af2

Please sign in to comment.