diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index b32e67bd7..612c5b356 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -5,12 +5,30 @@ import textwrap from pathlib import Path from typing import Dict from typing import Generator +from typing import Optional +from typing import overload from typing import Type +from typing import TypeVar import pytest from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester +T = TypeVar("T") + + +class SomeDescriptor: + @overload + def __get__(self, instance: None, owner: type) -> int: + ... + + @overload + def __get__(self, instance: T, owner: Optional[Type[T]] = ...) -> int: + ... + + def __get__(self, instance: Optional[T], owner: Optional[Type[T]] = None) -> int: + return 1 + @pytest.fixture def mp() -> Generator[MonkeyPatch, None, None]: @@ -22,15 +40,21 @@ def mp() -> Generator[MonkeyPatch, None, None]: def test_setattr() -> None: + import inspect + class A: x = 1 + y = SomeDescriptor() monkeypatch = MonkeyPatch() pytest.raises(AttributeError, monkeypatch.setattr, A, "notexists", 2) - monkeypatch.setattr(A, "y", 2, raising=False) - assert A.y == 2 # type: ignore + monkeypatch.setattr(A, "w", 2, raising=False) + assert A.w == 2 # type: ignore monkeypatch.undo() - assert not hasattr(A, "y") + assert not hasattr(A, "w") + + with pytest.raises(TypeError): + monkeypatch.setattr(A, "w") # type: ignore[call-overload] monkeypatch = MonkeyPatch() monkeypatch.setattr(A, "x", 2) @@ -44,8 +68,23 @@ def test_setattr() -> None: monkeypatch.undo() # double-undo makes no modification assert A.x == 5 - with pytest.raises(TypeError): - monkeypatch.setattr(A, "y") # type: ignore[call-overload] + # Test that inherited attributes don't get written into the target's instance + # dictionary. + a = A() + monkeypatch = MonkeyPatch() + assert "x" not in vars(a) + monkeypatch.setattr(a, "x", 2) + monkeypatch.undo() + assert "x" not in vars(a) + + # Test that class/instance descriptors don't get bound and written into the target's + # dictionary. + for obj in (A, A()): + monkeypatch = MonkeyPatch() + assert isinstance(inspect.getattr_static(obj, "y"), SomeDescriptor) + monkeypatch.setattr(obj, "y", 2) + monkeypatch.undo() + assert isinstance(inspect.getattr_static(obj, "y"), SomeDescriptor) class TestSetattrWithImportPath: @@ -96,8 +135,11 @@ class TestSetattrWithImportPath: def test_delattr() -> None: + import inspect + class A: x = 1 + y = SomeDescriptor() monkeypatch = MonkeyPatch() monkeypatch.delattr(A, "x") @@ -106,14 +148,22 @@ def test_delattr() -> None: assert A.x == 1 monkeypatch = MonkeyPatch() + pytest.raises(AttributeError, monkeypatch.delattr, A, "w") + monkeypatch.delattr(A, "w", raising=False) monkeypatch.delattr(A, "x") - pytest.raises(AttributeError, monkeypatch.delattr, A, "y") - monkeypatch.delattr(A, "y", raising=False) monkeypatch.setattr(A, "x", 5, raising=False) assert A.x == 5 monkeypatch.undo() assert A.x == 1 + # Test that (non-inherited) class descriptors don't get bound and written into the + # target's dictionary. + monkeypatch = MonkeyPatch() + assert isinstance(inspect.getattr_static(A, "y"), SomeDescriptor) + monkeypatch.delattr(A, "y") + monkeypatch.undo() + assert isinstance(inspect.getattr_static(A, "y"), SomeDescriptor) + def test_setitem() -> None: d = {"x": 1}