Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ Milan Lesnek
minbang930
Minh Dau
Miro Hrončok
Muhammad Sharif
Mulat Mekonen
mrbean-bremen
Nathan Goldbaum
Expand Down
3 changes: 3 additions & 0 deletions changelog/15157.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix string-based :meth:`pytest.MonkeyPatch.setattr` and
:meth:`pytest.MonkeyPatch.delattr` for cached submodules whose attribute is missing
from the parent package.
3 changes: 1 addition & 2 deletions src/_pytest/monkeypatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,13 @@ def resolve(name: str) -> object:
# We use explicit un-nesting of the handling block in order
# to avoid nested exceptions.
try:
importlib.import_module(used)
found = importlib.import_module(used)
except ImportError as ex:
expected = str(ex).split()[-1]
if expected == used:
raise
else:
raise ImportError(f"import error in {used}: {ex}") from ex
found = annotated_getattr(found, part, used)
return found


Expand Down
24 changes: 24 additions & 0 deletions testing/test_monkeypatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import annotations

from collections.abc import Generator
import importlib
import os
from pathlib import Path
import re
Expand Down Expand Up @@ -53,6 +54,29 @@ class A:


class TestSetattrWithImportPath:
@pytest.mark.parametrize("operation", ["setattr", "delattr"])
def test_cached_module_without_parent_attribute(
self, pytester: Pytester, monkeypatch: MonkeyPatch, operation: str
) -> None:
package_path = pytester.mkdir("cached_target_package")
(package_path / "__init__.py").write_text("", encoding="utf-8")
(package_path / "child.py").write_text("value = 1", encoding="utf-8")
monkeypatch.syspath_prepend(str(pytester.path))
child = importlib.import_module("cached_target_package.child")
package = importlib.import_module("cached_target_package")
monkeypatch.delattr(package, "child")

# A cached module is still importable without its parent attribute.
assert importlib.import_module("cached_target_package.child") is child
with monkeypatch.context() as mp:
if operation == "setattr":
mp.setattr("cached_target_package.child.value", 2)
assert child.value == 2
else:
mp.delattr("cached_target_package.child.value")
assert not hasattr(child, "value")
assert child.value == 1

def test_string_expression(self, monkeypatch: MonkeyPatch) -> None:
with monkeypatch.context() as mp:
mp.setattr("os.path.abspath", lambda x: "hello2")
Expand Down
Loading