From 6a3fd7ff61853d386bbacfcd8aa2d9366a033f1c Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Thu, 13 Jul 2023 21:33:37 +0200 Subject: [PATCH 1/5] Fix @patch when `new` is missing Closes: #10457 --- stdlib/unittest/mock.pyi | 12 ++++++++++-- test_cases/stdlib/check_unittest.py | 17 ++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/stdlib/unittest/mock.pyi b/stdlib/unittest/mock.pyi index 0ed0701cc450..c64e8c1137cd 100644 --- a/stdlib/unittest/mock.pyi +++ b/stdlib/unittest/mock.pyi @@ -2,7 +2,7 @@ import sys from collections.abc import Awaitable, Callable, Coroutine, Iterable, Mapping, Sequence from contextlib import _GeneratorContextManager from types import TracebackType -from typing import Any, Generic, TypeVar, overload +from typing import Any, Concatenate, Generic, TypeVar, overload from typing_extensions import Final, Literal, ParamSpec, Self, TypeAlias _T = TypeVar("_T") @@ -257,6 +257,14 @@ class _patch(Generic[_T]): def start(self) -> _T: ... def stop(self) -> None: ... +class _patch_default_new(_patch[MagicMock]): + @overload + def __call__(self, func: _TT) -> _TT: ... + # Can't use the following as ParamSpec is only allowed as last parameter: + # def __call__(self, func: Callable[_P, _R]) -> Callable[Concatenate[_P, MagicMock], _R]: ... + @overload + def __call__(self, func: Callable[..., _R]) -> Callable[..., _R]: ... + class _patch_dict: in_dict: Any values: Any @@ -307,7 +315,7 @@ class _patcher: autospec: Any | None = ..., new_callable: Any | None = ..., **kwargs: Any, - ) -> _patch[_Mock]: ... + ) -> _patch_default_new: ... @overload @staticmethod def object( # type: ignore[misc] diff --git a/test_cases/stdlib/check_unittest.py b/test_cases/stdlib/check_unittest.py index 438250670559..4134a3182b7a 100644 --- a/test_cases/stdlib/check_unittest.py +++ b/test_cases/stdlib/check_unittest.py @@ -5,7 +5,7 @@ from decimal import Decimal from fractions import Fraction from typing_extensions import assert_type -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, patch case = unittest.TestCase() @@ -94,13 +94,20 @@ def __gt__(self, other: Bacon) -> bool: ### -@patch("sys.exit", new=Mock()) -def f(i: int) -> str: +@patch("sys.exit") +def f_default(i: int, mock: MagicMock) -> str: + return "asdf" + + +@patch("sys.exit", new=42) +def f_new(i: int) -> str: return "asdf" -assert_type(f(1), str) -f("a") # type: ignore +assert_type(f_default(1), str) +f_default("a") # Not an error due to ParamSpec limitations +assert_type(f_new(1), str) +f_new("a") # type: ignore[arg-type] @patch("sys.exit", new=Mock()) From 355e9d5f9123839c2019ed996277bc536a0fe9f9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 13 Jul 2023 19:37:00 +0000 Subject: [PATCH 2/5] [pre-commit.ci] auto fixes from pre-commit.com hooks --- stdlib/unittest/mock.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stdlib/unittest/mock.pyi b/stdlib/unittest/mock.pyi index c64e8c1137cd..b43d51eed3cc 100644 --- a/stdlib/unittest/mock.pyi +++ b/stdlib/unittest/mock.pyi @@ -2,7 +2,7 @@ import sys from collections.abc import Awaitable, Callable, Coroutine, Iterable, Mapping, Sequence from contextlib import _GeneratorContextManager from types import TracebackType -from typing import Any, Concatenate, Generic, TypeVar, overload +from typing import Any, Generic, TypeVar, overload from typing_extensions import Final, Literal, ParamSpec, Self, TypeAlias _T = TypeVar("_T") From ddbacf781fd4541d04eb9cbb697c8ee465e9a20a Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Thu, 13 Jul 2023 21:42:16 +0200 Subject: [PATCH 3/5] Add documentation for the hacks we use --- stdlib/unittest/mock.pyi | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/stdlib/unittest/mock.pyi b/stdlib/unittest/mock.pyi index b43d51eed3cc..1cb97458fd93 100644 --- a/stdlib/unittest/mock.pyi +++ b/stdlib/unittest/mock.pyi @@ -234,6 +234,8 @@ class _patch(Generic[_T]): def copy(self) -> _patch[_T]: ... @overload def __call__(self, func: _TT) -> _TT: ... + # If new==DEFAULT, this should add a MagicMock parameter to the function + # arguments. See the _patch_default_new class below for this functionality. @overload def __call__(self, func: Callable[_P, _R]) -> Callable[_P, _R]: ... if sys.version_info >= (3, 8): @@ -257,6 +259,9 @@ class _patch(Generic[_T]): def start(self) -> _T: ... def stop(self) -> None: ... +# This class does not exist at runtime, it's a hack to make this work: +# @patch("foo") +# def bar(..., mock: MagicMock) -> None: ... class _patch_default_new(_patch[MagicMock]): @overload def __call__(self, func: _TT) -> _TT: ... @@ -286,6 +291,8 @@ if sys.version_info >= (3, 8): else: _Mock: TypeAlias = MagicMock +# This class does not exist at runtime, it's a hack to add methods to the +# patch() function. class _patcher: TEST_PREFIX: str dict: type[_patch_dict] From 9641f88c59f812736b0561cbfaebcc73bf8b7222 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Thu, 13 Jul 2023 21:44:55 +0200 Subject: [PATCH 4/5] Rename test functions --- test_cases/stdlib/check_unittest.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test_cases/stdlib/check_unittest.py b/test_cases/stdlib/check_unittest.py index 4134a3182b7a..3a9edafef051 100644 --- a/test_cases/stdlib/check_unittest.py +++ b/test_cases/stdlib/check_unittest.py @@ -95,19 +95,19 @@ def __gt__(self, other: Bacon) -> bool: @patch("sys.exit") -def f_default(i: int, mock: MagicMock) -> str: +def f_default_new(i: int, mock: MagicMock) -> str: return "asdf" @patch("sys.exit", new=42) -def f_new(i: int) -> str: +def f_explicit_new(i: int) -> str: return "asdf" -assert_type(f_default(1), str) -f_default("a") # Not an error due to ParamSpec limitations -assert_type(f_new(1), str) -f_new("a") # type: ignore[arg-type] +assert_type(f_default_new(1), str) +f_default_new("a") # Not an error due to ParamSpec limitations +assert_type(f_explicit_new(1), str) +f_explicit_new("a") # type: ignore[arg-type] @patch("sys.exit", new=Mock()) From 488dea521bf56ef9c0be80a82ac03fa73066bad8 Mon Sep 17 00:00:00 2001 From: Sebastian Rittau Date: Thu, 13 Jul 2023 21:46:30 +0200 Subject: [PATCH 5/5] Fix _patch_default_new type arg --- stdlib/unittest/mock.pyi | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/stdlib/unittest/mock.pyi b/stdlib/unittest/mock.pyi index 1cb97458fd93..db1cc7d9bfc9 100644 --- a/stdlib/unittest/mock.pyi +++ b/stdlib/unittest/mock.pyi @@ -259,10 +259,15 @@ class _patch(Generic[_T]): def start(self) -> _T: ... def stop(self) -> None: ... +if sys.version_info >= (3, 8): + _Mock: TypeAlias = MagicMock | AsyncMock +else: + _Mock: TypeAlias = MagicMock + # This class does not exist at runtime, it's a hack to make this work: # @patch("foo") # def bar(..., mock: MagicMock) -> None: ... -class _patch_default_new(_patch[MagicMock]): +class _patch_default_new(_patch[_Mock]): @overload def __call__(self, func: _TT) -> _TT: ... # Can't use the following as ParamSpec is only allowed as last parameter: @@ -286,11 +291,6 @@ class _patch_dict: start: Any stop: Any -if sys.version_info >= (3, 8): - _Mock: TypeAlias = MagicMock | AsyncMock -else: - _Mock: TypeAlias = MagicMock - # This class does not exist at runtime, it's a hack to add methods to the # patch() function. class _patcher: