From dc7a4503ab5bc74015263ba669f2148a46a4032c Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Sat, 13 May 2023 16:10:35 -0500 Subject: [PATCH 01/14] monkeypatch: add support for TypedDict --- src/_pytest/monkeypatch.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index c6e29ac7642..2033a76f712 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -129,7 +129,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[Union[MutableMapping[Any, Any], "TypedDict[Any, Any]", object, object]] = [] self._cwd: Optional[str] = None self._savesyspath: Optional[List[str]] = None @@ -290,12 +290,12 @@ def delattr( 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: Union[MutableMapping[K, V], "TypedDict[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 - def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None: + def delitem(self, dic: Union[MutableMapping[K, V], "TypedDict[K, V"], name: K, raising: bool = True) -> None: """Delete ``name`` from dict. Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to From 51ed638e5b4436d6931a5fb29d92f7f70c47b776 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Sat, 13 May 2023 16:15:10 -0500 Subject: [PATCH 02/14] Update changelog/author list --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 438be759814..a6112565e34 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 From bc60c29722b810c70669def9acbf09a7f67b424d Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Sat, 13 May 2023 16:15:21 -0500 Subject: [PATCH 03/14] Add changelog --- changelog/10999.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/10999.bugfix.rst diff --git a/changelog/10999.bugfix.rst b/changelog/10999.bugfix.rst new file mode 100644 index 00000000000..1eeae02b12f --- /dev/null +++ b/changelog/10999.bugfix.rst @@ -0,0 +1 @@ +Fixed bug where monkeypatch setitem/delitem type annotations didn't support typing.TypedDict. From 2e6e5f7288e05a873c5b7486fce0a35f3039490f Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Sat, 13 May 2023 16:22:38 -0500 Subject: [PATCH 04/14] Fix syntax errors --- src/_pytest/monkeypatch.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 2033a76f712..e39905d944e 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -4,6 +4,7 @@ import sys import warnings from contextlib import contextmanager +import typing from typing import Any from typing import Generator from typing import List @@ -129,7 +130,7 @@ class MonkeyPatch: def __init__(self) -> None: self._setattr: List[Tuple[object, str, object]] = [] - self._setitem: List[Tuple[Union[MutableMapping[Any, Any], "TypedDict[Any, Any]", object, object]] = [] + self._setitem: List[Tuple[Union[MutableMapping[Any, Any], "typing.TypedDict[Any, Any]"], object, object]] = [] self._cwd: Optional[str] = None self._savesyspath: Optional[List[str]] = None @@ -290,12 +291,12 @@ def delattr( self._setattr.append((target, name, oldval)) delattr(target, name) - def setitem(self, dic: Union[MutableMapping[K, V], "TypedDict[K, V]"], name: K, value: V) -> None: + def setitem(self, dic: Union[MutableMapping[K, V], "typing.TypedDict[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 - def delitem(self, dic: Union[MutableMapping[K, V], "TypedDict[K, V"], name: K, raising: bool = True) -> None: + def delitem(self, dic: Union[MutableMapping[K, V], "typing.TypedDict[K, V]"], name: K, raising: bool = True) -> None: """Delete ``name`` from dict. Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to From 6092c2455fd8c462e795b457afefbd7a2cbfa205 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 13 May 2023 21:23:23 +0000 Subject: [PATCH 05/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_pytest/monkeypatch.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index e39905d944e..e988f59a266 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -2,9 +2,9 @@ import os import re import sys +import typing import warnings from contextlib import contextmanager -import typing from typing import Any from typing import Generator from typing import List @@ -130,7 +130,13 @@ class MonkeyPatch: def __init__(self) -> None: self._setattr: List[Tuple[object, str, object]] = [] - self._setitem: List[Tuple[Union[MutableMapping[Any, Any], "typing.TypedDict[Any, Any]"], object, object]] = [] + self._setitem: List[ + Tuple[ + Union[MutableMapping[Any, Any], "typing.TypedDict[Any, Any]"], + object, + object, + ] + ] = [] self._cwd: Optional[str] = None self._savesyspath: Optional[List[str]] = None @@ -291,12 +297,22 @@ def delattr( self._setattr.append((target, name, oldval)) delattr(target, name) - def setitem(self, dic: Union[MutableMapping[K, V], "typing.TypedDict[K, V]"], name: K, value: V) -> None: + def setitem( + self, + dic: Union[MutableMapping[K, V], "typing.TypedDict[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 - def delitem(self, dic: Union[MutableMapping[K, V], "typing.TypedDict[K, V]"], name: K, raising: bool = True) -> None: + def delitem( + self, + dic: Union[MutableMapping[K, V], "typing.TypedDict[K, V]"], + name: K, + raising: bool = True, + ) -> None: """Delete ``name`` from dict. Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to From 3a7f1179fa4ffcc30b3f718d6edf5c8d718139cc Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Sat, 13 May 2023 16:31:57 -0500 Subject: [PATCH 06/14] Test TypedDict support --- testing/typing_checks.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/testing/typing_checks.py b/testing/typing_checks.py index d15b3988bb5..80b8c9f8250 100644 --- a/testing/typing_checks.py +++ b/testing/typing_checks.py @@ -4,11 +4,13 @@ none of the code triggers any mypy errors. """ import contextlib +import sys from typing import Optional from typing_extensions import assert_type import pytest +from _pytest.monkeypatch import MonkeyPatch # Issue #7488. @@ -29,6 +31,16 @@ def check_parametrize_ids_callable(func) -> None: pass +# Issue #10999. +@pytest.mark.skipif(sys.version_info < (3, 8), reason="TypedDict introduced in Python 3.8") +def check_monkeypatch_typeddict(monkeypatch: MonkeyPatch) -> None: + from typing import TypedDict + + Foo = TypedDict("Foo", {"x": int, "y": float}) + a: Foo = {"x": 1, "y": 3.14} + monkeypatch.setitem("x", 2) + monkeypatch.delitem("y") + def check_raises_is_a_context_manager(val: bool) -> None: with pytest.raises(RuntimeError) if val else contextlib.nullcontext() as excinfo: pass From a5a8e35bc379e7b445a3892e6a0211c4db2518d3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 13 May 2023 21:32:39 +0000 Subject: [PATCH 07/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/typing_checks.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/testing/typing_checks.py b/testing/typing_checks.py index 80b8c9f8250..1f69430ee7c 100644 --- a/testing/typing_checks.py +++ b/testing/typing_checks.py @@ -32,15 +32,20 @@ def check_parametrize_ids_callable(func) -> None: # Issue #10999. -@pytest.mark.skipif(sys.version_info < (3, 8), reason="TypedDict introduced in Python 3.8") +@pytest.mark.skipif( + sys.version_info < (3, 8), reason="TypedDict introduced in Python 3.8" +) def check_monkeypatch_typeddict(monkeypatch: MonkeyPatch) -> None: from typing import TypedDict - Foo = TypedDict("Foo", {"x": int, "y": float}) + class Foo(TypedDict): + x: int + y: float a: Foo = {"x": 1, "y": 3.14} monkeypatch.setitem("x", 2) monkeypatch.delitem("y") + def check_raises_is_a_context_manager(val: bool) -> None: with pytest.raises(RuntimeError) if val else contextlib.nullcontext() as excinfo: pass From 8bc212caf7ed89d6c6fa2796b07f52e0e1af7e64 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Sat, 13 May 2023 17:15:19 -0500 Subject: [PATCH 08/14] MutableMapping -> Mapping --- src/_pytest/monkeypatch.py | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index e988f59a266..1df864b05c7 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -2,13 +2,12 @@ import os import re import sys -import typing import warnings from contextlib import contextmanager from typing import Any from typing import Generator from typing import List -from typing import MutableMapping +from typing import Mapping from typing import Optional from typing import overload from typing import Tuple @@ -130,13 +129,7 @@ class MonkeyPatch: def __init__(self) -> None: self._setattr: List[Tuple[object, str, object]] = [] - self._setitem: List[ - Tuple[ - Union[MutableMapping[Any, Any], "typing.TypedDict[Any, Any]"], - object, - object, - ] - ] = [] + self._setitem: List[Tuple[Mapping[Any, Any], object, object]] = [] self._cwd: Optional[str] = None self._savesyspath: Optional[List[str]] = None @@ -297,21 +290,12 @@ def delattr( self._setattr.append((target, name, oldval)) delattr(target, name) - def setitem( - self, - dic: Union[MutableMapping[K, V], "typing.TypedDict[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 - def delitem( - self, - dic: Union[MutableMapping[K, V], "typing.TypedDict[K, V]"], - name: K, - raising: bool = True, + def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True, ) -> None: """Delete ``name`` from dict. @@ -353,7 +337,7 @@ def delenv(self, name: str, raising: bool = True) -> None: Raises ``KeyError`` if it does not exist, unless ``raising`` is set to False. """ - environ: MutableMapping[str, str] = os.environ + environ: Mapping[str, str] = os.environ self.delitem(environ, name, raising=raising) def syspath_prepend(self, path) -> None: From 713e801e6fec8366be9a1c595bf8d319b98db695 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 13 May 2023 22:16:01 +0000 Subject: [PATCH 09/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/_pytest/monkeypatch.py | 6 +++++- testing/typing_checks.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 1df864b05c7..6d63da443d3 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -295,7 +295,11 @@ def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None: self._setitem.append((dic, name, dic.get(name, notset))) dic[name] = value - def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True, + def delitem( + self, + dic: Mapping[K, V], + name: K, + raising: bool = True, ) -> None: """Delete ``name`` from dict. diff --git a/testing/typing_checks.py b/testing/typing_checks.py index 1f69430ee7c..765837bb4b2 100644 --- a/testing/typing_checks.py +++ b/testing/typing_checks.py @@ -41,6 +41,7 @@ def check_monkeypatch_typeddict(monkeypatch: MonkeyPatch) -> None: class Foo(TypedDict): x: int y: float + a: Foo = {"x": 1, "y": 3.14} monkeypatch.setitem("x", 2) monkeypatch.delitem("y") From 42fddd3b0c8b8ae951c096b0a9082d933351a4b1 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Sat, 13 May 2023 21:16:16 -0500 Subject: [PATCH 10/14] Apply suggestions from code review Co-authored-by: Ran Benita --- changelog/10999.bugfix.rst | 2 +- src/_pytest/monkeypatch.py | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/changelog/10999.bugfix.rst b/changelog/10999.bugfix.rst index 1eeae02b12f..08c68da01bf 100644 --- a/changelog/10999.bugfix.rst +++ b/changelog/10999.bugfix.rst @@ -1 +1 @@ -Fixed bug where monkeypatch setitem/delitem type annotations didn't support typing.TypedDict. +The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments. diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 6d63da443d3..c4a72e49d39 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -295,12 +295,7 @@ def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None: self._setitem.append((dic, name, dic.get(name, notset))) dic[name] = value - def delitem( - self, - dic: Mapping[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 @@ -341,7 +336,7 @@ def delenv(self, name: str, raising: bool = True) -> None: Raises ``KeyError`` if it does not exist, unless ``raising`` is set to False. """ - environ: Mapping[str, str] = os.environ + environ: MutableMapping[str, str] = os.environ self.delitem(environ, name, raising=raising) def syspath_prepend(self, path) -> None: From a2f244097524f6599b113db23c7fe8e69db18ed1 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Sat, 13 May 2023 21:17:36 -0500 Subject: [PATCH 11/14] Simpler type checks --- testing/typing_checks.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/testing/typing_checks.py b/testing/typing_checks.py index 765837bb4b2..19b9741e75d 100644 --- a/testing/typing_checks.py +++ b/testing/typing_checks.py @@ -10,7 +10,7 @@ from typing_extensions import assert_type import pytest -from _pytest.monkeypatch import MonkeyPatch +from pytest import MonkeyPatch # Issue #7488. @@ -32,9 +32,6 @@ def check_parametrize_ids_callable(func) -> None: # Issue #10999. -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="TypedDict introduced in Python 3.8" -) def check_monkeypatch_typeddict(monkeypatch: MonkeyPatch) -> None: from typing import TypedDict From 93f0865b3aeae9f818b0c3c1195b6f42542ec738 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Sat, 13 May 2023 21:17:55 -0500 Subject: [PATCH 12/14] Unused import --- testing/typing_checks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/typing_checks.py b/testing/typing_checks.py index 19b9741e75d..8f91a9fdf92 100644 --- a/testing/typing_checks.py +++ b/testing/typing_checks.py @@ -4,7 +4,6 @@ none of the code triggers any mypy errors. """ import contextlib -import sys from typing import Optional from typing_extensions import assert_type From 4413aeebd32514b30ce61e90de3830f04a9532ba Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Sat, 13 May 2023 21:21:04 -0500 Subject: [PATCH 13/14] Fix type hints --- src/_pytest/monkeypatch.py | 1 + testing/typing_checks.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index c4a72e49d39..7d981d95231 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -8,6 +8,7 @@ from typing import Generator from typing import List from typing import Mapping +from typing import MutableMapping from typing import Optional from typing import overload from typing import Tuple diff --git a/testing/typing_checks.py b/testing/typing_checks.py index 8f91a9fdf92..57f2bae475f 100644 --- a/testing/typing_checks.py +++ b/testing/typing_checks.py @@ -39,8 +39,8 @@ class Foo(TypedDict): y: float a: Foo = {"x": 1, "y": 3.14} - monkeypatch.setitem("x", 2) - monkeypatch.delitem("y") + monkeypatch.setitem(a, "x", 2) + monkeypatch.delitem(a, "y") def check_raises_is_a_context_manager(val: bool) -> None: From 4f93d50303b9083fa7f59b0b10431ea2ce6919a4 Mon Sep 17 00:00:00 2001 From: "Adam J. Stewart" Date: Sun, 14 May 2023 10:56:13 -0500 Subject: [PATCH 14/14] Ignore mypy errors --- src/_pytest/monkeypatch.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 7d981d95231..9e51ff33538 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -294,7 +294,8 @@ def delattr( 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: Mapping[K, V], name: K, raising: bool = True) -> None: """Delete ``name`` from dict. @@ -307,7 +308,8 @@ def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None: 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``. @@ -402,11 +404,13 @@ def undo(self) -> None: 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