Skip to content

Commit a4d7254

Browse files
github-actions[bot]nicoddemus
andauthoredJul 3, 2023
[7.4.x] Fix duplicated imports with importlib mode and doctest-modules (#11164)
Co-authored-by: Bruno Oliveira <[email protected]>
1 parent b6c5578 commit a4d7254

File tree

4 files changed

+58
-8
lines changed

4 files changed

+58
-8
lines changed
 

‎changelog/10811.bugfix.rst

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed issue when using ``--import-mode=importlib`` together with ``--doctest-modules`` that caused modules
2+
to be imported more than once, causing problems with modules that have import side effects.

‎src/_pytest/pathlib.py

+2
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,8 @@ def import_path(
523523

524524
if mode is ImportMode.importlib:
525525
module_name = module_name_from_path(path, root)
526+
with contextlib.suppress(KeyError):
527+
return sys.modules[module_name]
526528

527529
for meta_importer in sys.meta_path:
528530
spec = meta_importer.find_spec(module_name, [str(path.parent)])

‎testing/acceptance_test.py

+35
Original file line numberDiff line numberDiff line change
@@ -1317,3 +1317,38 @@ def test_stuff():
13171317
)
13181318
res = pytester.runpytest()
13191319
res.stdout.fnmatch_lines(["*Did you mean to use `assert` instead of `return`?*"])
1320+
1321+
1322+
def test_doctest_and_normal_imports_with_importlib(pytester: Pytester) -> None:
1323+
"""
1324+
Regression test for #10811: previously import_path with ImportMode.importlib would
1325+
not return a module if already in sys.modules, resulting in modules being imported
1326+
multiple times, which causes problems with modules that have import side effects.
1327+
"""
1328+
# Uses the exact reproducer form #10811, given it is very minimal
1329+
# and illustrates the problem well.
1330+
pytester.makepyfile(
1331+
**{
1332+
"pmxbot/commands.py": "from . import logging",
1333+
"pmxbot/logging.py": "",
1334+
"tests/__init__.py": "",
1335+
"tests/test_commands.py": """
1336+
import importlib
1337+
from pmxbot import logging
1338+
1339+
class TestCommands:
1340+
def test_boo(self):
1341+
assert importlib.import_module('pmxbot.logging') is logging
1342+
""",
1343+
}
1344+
)
1345+
pytester.makeini(
1346+
"""
1347+
[pytest]
1348+
addopts=
1349+
--doctest-modules
1350+
--import-mode importlib
1351+
"""
1352+
)
1353+
result = pytester.runpytest_subprocess()
1354+
result.stdout.fnmatch_lines("*1 passed*")

‎testing/test_pathlib.py

+19-8
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from types import ModuleType
88
from typing import Any
99
from typing import Generator
10+
from typing import Iterator
1011

1112
import pytest
1213
from _pytest.monkeypatch import MonkeyPatch
@@ -282,29 +283,36 @@ def test_invalid_path(self, tmp_path: Path) -> None:
282283
import_path(tmp_path / "invalid.py", root=tmp_path)
283284

284285
@pytest.fixture
285-
def simple_module(self, tmp_path: Path) -> Path:
286-
fn = tmp_path / "_src/tests/mymod.py"
286+
def simple_module(
287+
self, tmp_path: Path, request: pytest.FixtureRequest
288+
) -> Iterator[Path]:
289+
name = f"mymod_{request.node.name}"
290+
fn = tmp_path / f"_src/tests/{name}.py"
287291
fn.parent.mkdir(parents=True)
288292
fn.write_text("def foo(x): return 40 + x", encoding="utf-8")
289-
return fn
293+
module_name = module_name_from_path(fn, root=tmp_path)
294+
yield fn
295+
sys.modules.pop(module_name, None)
290296

291-
def test_importmode_importlib(self, simple_module: Path, tmp_path: Path) -> None:
297+
def test_importmode_importlib(
298+
self, simple_module: Path, tmp_path: Path, request: pytest.FixtureRequest
299+
) -> None:
292300
"""`importlib` mode does not change sys.path."""
293301
module = import_path(simple_module, mode="importlib", root=tmp_path)
294302
assert module.foo(2) == 42 # type: ignore[attr-defined]
295303
assert str(simple_module.parent) not in sys.path
296304
assert module.__name__ in sys.modules
297-
assert module.__name__ == "_src.tests.mymod"
305+
assert module.__name__ == f"_src.tests.mymod_{request.node.name}"
298306
assert "_src" in sys.modules
299307
assert "_src.tests" in sys.modules
300308

301-
def test_importmode_twice_is_different_module(
309+
def test_remembers_previous_imports(
302310
self, simple_module: Path, tmp_path: Path
303311
) -> None:
304-
"""`importlib` mode always returns a new module."""
312+
"""`importlib` mode called remembers previous module (#10341, #10811)."""
305313
module1 = import_path(simple_module, mode="importlib", root=tmp_path)
306314
module2 = import_path(simple_module, mode="importlib", root=tmp_path)
307-
assert module1 is not module2
315+
assert module1 is module2
308316

309317
def test_no_meta_path_found(
310318
self, simple_module: Path, monkeypatch: MonkeyPatch, tmp_path: Path
@@ -317,6 +325,9 @@ def test_no_meta_path_found(
317325
# mode='importlib' fails if no spec is found to load the module
318326
import importlib.util
319327

328+
# Force module to be re-imported.
329+
del sys.modules[module.__name__]
330+
320331
monkeypatch.setattr(
321332
importlib.util, "spec_from_file_location", lambda *args: None
322333
)

0 commit comments

Comments
 (0)
Please sign in to comment.