From 3b5b3cf50ee514773c14626419b472625e76e438 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Sun, 14 May 2023 14:17:00 -0500 Subject: [PATCH] monkeypatch: add support for TypedDict (#11000) --- AUTHORS | 1 + changelog/10999.bugfix.rst | 1 + src/_pytest/monkeypatch.py | 19 ++++++++++++------- testing/typing_checks.py | 14 ++++++++++++++ 4 files changed, 28 insertions(+), 7 deletions(-) create mode 100644 changelog/10999.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 438be7598..a6112565e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -8,6 +8,7 @@ Abdeali JK Abdelrahman Elbehery Abhijeet Kasurde Adam Johnson +Adam Stewart Adam Uhlir Ahn Ki-Wook Akiomi Kamakura diff --git a/changelog/10999.bugfix.rst b/changelog/10999.bugfix.rst new file mode 100644 index 000000000..08c68da01 --- /dev/null +++ b/changelog/10999.bugfix.rst @@ -0,0 +1 @@ +The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments. diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index c6e29ac76..9e51ff335 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -7,6 +7,7 @@ from contextlib import contextmanager from typing import Any from typing import Generator from typing import List +from typing import Mapping from typing import MutableMapping from typing import Optional from typing import overload @@ -129,7 +130,7 @@ class MonkeyPatch: def __init__(self) -> None: self._setattr: List[Tuple[object, str, object]] = [] - self._setitem: List[Tuple[MutableMapping[Any, Any], object, object]] = [] + self._setitem: List[Tuple[Mapping[Any, Any], object, object]] = [] self._cwd: Optional[str] = None self._savesyspath: Optional[List[str]] = None @@ -290,12 +291,13 @@ class MonkeyPatch: self._setattr.append((target, name, oldval)) delattr(target, name) - def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None: + def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None: """Set dictionary entry ``name`` to value.""" self._setitem.append((dic, name, dic.get(name, notset))) - dic[name] = value + # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict + dic[name] = value # type: ignore[index] - def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None: + def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None: """Delete ``name`` from dict. Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to @@ -306,7 +308,8 @@ class MonkeyPatch: raise KeyError(name) else: self._setitem.append((dic, name, dic.get(name, notset))) - del dic[name] + # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict + del dic[name] # type: ignore[attr-defined] def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None: """Set environment variable ``name`` to ``value``. @@ -401,11 +404,13 @@ class MonkeyPatch: for dictionary, key, value in reversed(self._setitem): if value is notset: try: - del dictionary[key] + # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict + del dictionary[key] # type: ignore[attr-defined] except KeyError: pass # Was already deleted, so we have the desired state. else: - dictionary[key] = value + # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict + dictionary[key] = value # type: ignore[index] self._setitem[:] = [] if self._savesyspath is not None: sys.path[:] = self._savesyspath diff --git a/testing/typing_checks.py b/testing/typing_checks.py index d15b3988b..57f2bae47 100644 --- a/testing/typing_checks.py +++ b/testing/typing_checks.py @@ -9,6 +9,7 @@ from typing import Optional from typing_extensions import assert_type import pytest +from pytest import MonkeyPatch # Issue #7488. @@ -29,6 +30,19 @@ def check_parametrize_ids_callable(func) -> None: pass +# Issue #10999. +def check_monkeypatch_typeddict(monkeypatch: MonkeyPatch) -> None: + from typing import TypedDict + + class Foo(TypedDict): + x: int + y: float + + a: Foo = {"x": 1, "y": 3.14} + monkeypatch.setitem(a, "x", 2) + monkeypatch.delitem(a, "y") + + def check_raises_is_a_context_manager(val: bool) -> None: with pytest.raises(RuntimeError) if val else contextlib.nullcontext() as excinfo: pass