[7.3.x] monkeypatch: add support for TypedDict

This commit is contained in:
Adam J. Stewart 2023-05-14 14:17:00 -05:00 committed by pytest bot
parent 8082d27d11
commit bcada5138a
4 changed files with 28 additions and 7 deletions

View File

@ -8,6 +8,7 @@ Abdeali JK
Abdelrahman Elbehery Abdelrahman Elbehery
Abhijeet Kasurde Abhijeet Kasurde
Adam Johnson Adam Johnson
Adam Stewart
Adam Uhlir Adam Uhlir
Ahn Ki-Wook Ahn Ki-Wook
Akiomi Kamakura Akiomi Kamakura

View File

@ -0,0 +1 @@
The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments.

View File

@ -7,6 +7,7 @@ from contextlib import contextmanager
from typing import Any from typing import Any
from typing import Generator from typing import Generator
from typing import List from typing import List
from typing import Mapping
from typing import MutableMapping from typing import MutableMapping
from typing import Optional from typing import Optional
from typing import overload from typing import overload
@ -129,7 +130,7 @@ class MonkeyPatch:
def __init__(self) -> None: def __init__(self) -> None:
self._setattr: List[Tuple[object, str, object]] = [] 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._cwd: Optional[str] = None
self._savesyspath: Optional[List[str]] = None self._savesyspath: Optional[List[str]] = None
@ -290,12 +291,13 @@ class MonkeyPatch:
self._setattr.append((target, name, oldval)) self._setattr.append((target, name, oldval))
delattr(target, name) 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.""" """Set dictionary entry ``name`` to value."""
self._setitem.append((dic, name, dic.get(name, notset))) 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. """Delete ``name`` from dict.
Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
@ -306,7 +308,8 @@ class MonkeyPatch:
raise KeyError(name) raise KeyError(name)
else: else:
self._setitem.append((dic, name, dic.get(name, notset))) 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: def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
"""Set environment variable ``name`` to ``value``. """Set environment variable ``name`` to ``value``.
@ -401,11 +404,13 @@ class MonkeyPatch:
for dictionary, key, value in reversed(self._setitem): for dictionary, key, value in reversed(self._setitem):
if value is notset: if value is notset:
try: 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: except KeyError:
pass # Was already deleted, so we have the desired state. pass # Was already deleted, so we have the desired state.
else: else:
dictionary[key] = value # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
dictionary[key] = value # type: ignore[index]
self._setitem[:] = [] self._setitem[:] = []
if self._savesyspath is not None: if self._savesyspath is not None:
sys.path[:] = self._savesyspath sys.path[:] = self._savesyspath

View File

@ -9,6 +9,7 @@ from typing import Optional
from typing_extensions import assert_type from typing_extensions import assert_type
import pytest import pytest
from pytest import MonkeyPatch
# Issue #7488. # Issue #7488.
@ -29,6 +30,19 @@ def check_parametrize_ids_callable(func) -> None:
pass 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: def check_raises_is_a_context_manager(val: bool) -> None:
with pytest.raises(RuntimeError) if val else contextlib.nullcontext() as excinfo: with pytest.raises(RuntimeError) if val else contextlib.nullcontext() as excinfo:
pass pass