From f5ca09a98f95673b73fa21f8352f90a067dd5f5e Mon Sep 17 00:00:00 2001 From: Leonardus Chen Date: Tue, 1 Jul 2025 20:51:38 +0700 Subject: [PATCH 1/5] Fix patch and patch.object when new_callable is not None --- stdlib/unittest/mock.pyi | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/stdlib/unittest/mock.pyi b/stdlib/unittest/mock.pyi index 9e353900f2d7..d4a2f72ce44c 100644 --- a/stdlib/unittest/mock.pyi +++ b/stdlib/unittest/mock.pyi @@ -303,7 +303,7 @@ class _patcher: create: bool = ..., spec_set: Any | None = ..., autospec: Any | None = ..., - new_callable: Any | None = ..., + new_callable: Callable[..., Any] | None = ..., **kwargs: Any, ) -> _patch[_T]: ... @overload @@ -315,7 +315,19 @@ class _patcher: create: bool = ..., spec_set: Any | None = ..., autospec: Any | None = ..., - new_callable: Any | None = ..., + new_callable: Callable[..., _T], + **kwargs: Any, + ) -> _patch[_T]: ... + @overload + def __call__( + self, + target: str, + *, + spec: Any | None = ..., + create: bool = ..., + spec_set: Any | None = ..., + autospec: Any | None = ..., + new_callable: None = ..., **kwargs: Any, ) -> _patch_default_new: ... @overload @@ -328,7 +340,7 @@ class _patcher: create: bool = ..., spec_set: Any | None = ..., autospec: Any | None = ..., - new_callable: Any | None = ..., + new_callable: Callable[..., Any] | None = ..., **kwargs: Any, ) -> _patch[_T]: ... @overload @@ -341,7 +353,20 @@ class _patcher: create: bool = ..., spec_set: Any | None = ..., autospec: Any | None = ..., - new_callable: Any | None = ..., + new_callable: Callable[..., _T], + **kwargs: Any, + ) -> _patch[_T]: ... + @overload + @staticmethod + def object( + target: Any, + attribute: str, + *, + spec: Any | None = ..., + create: bool = ..., + spec_set: Any | None = ..., + autospec: Any | None = ..., + new_callable: None = ..., **kwargs: Any, ) -> _patch[MagicMock | AsyncMock]: ... @staticmethod From fa964f367b691680f284f225e766903da3f79503 Mon Sep 17 00:00:00 2001 From: Leonardus Chen Date: Wed, 2 Jul 2025 22:06:23 +0700 Subject: [PATCH 2/5] Add new test cases --- stdlib/@tests/test_cases/check_unittest.py | 54 +++++++++++++++++++++- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/stdlib/@tests/test_cases/check_unittest.py b/stdlib/@tests/test_cases/check_unittest.py index 40c6efaa8ca0..3bbf9822ed5e 100644 --- a/stdlib/@tests/test_cases/check_unittest.py +++ b/stdlib/@tests/test_cases/check_unittest.py @@ -7,7 +7,7 @@ from fractions import Fraction from typing import TypedDict from typing_extensions import assert_type -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import MagicMock, Mock, patch, AsyncMock case = unittest.TestCase() @@ -153,12 +153,16 @@ def f_default_new(i: int, mock: MagicMock) -> str: def f_explicit_new(i: int) -> str: return "asdf" +@patch("sys.exit", new_callable=lambda: 42) +def f_explicit_new_callable(i: int, new_callable_ret: int) -> str: + return "asdf" 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] - +assert_type(f_explicit_new_callable(1), str) +f_explicit_new_callable("a") # Same as default new @patch("sys.exit", new=Mock()) class TestXYZ(unittest.TestCase): @@ -171,3 +175,49 @@ def method() -> int: assert_type(TestXYZ.attr, int) assert_type(TestXYZ.method(), int) + + +with patch("sys.exit") as default_new_enter: + assert_type(default_new_enter, MagicMock | AsyncMock) + +with patch("sys.exit", new=42) as explicit_new_enter: + assert_type(explicit_new_enter, int) + +with patch("sys.exit", new_callable=lambda: 42) as explicit_new_callable_enter: + assert_type(explicit_new_callable_enter, int) + + +### +# Tests for mock.patch.object +### + +@patch.object(Decimal, "exp") +def obj_f_default_new(i: int, mock: MagicMock) -> str: + return "asdf" + + +@patch.object(Decimal, "exp", new=42) +def obj_f_explicit_new(i: int) -> str: + return "asdf" + +@patch.object(Decimal, "exp", new_callable=lambda: 42) +def obj_f_explicit_new_callable(i: int, new_callable_ret: int) -> str: + return "asdf" + +assert_type(obj_f_default_new(1), str) +obj_f_default_new("a") # Not an error due to ParamSpec limitations +assert_type(obj_f_explicit_new(1), str) +obj_f_explicit_new("a") # type: ignore[arg-type] +assert_type(obj_f_explicit_new_callable(1), str) +obj_f_explicit_new_callable("a") # Same as default new + + + +with patch.object(Decimal, "exp") as obj_default_new_enter: + assert_type(obj_default_new_enter, MagicMock | AsyncMock) + +with patch.object(Decimal, "exp", new=42) as obj_explicit_new_enter: + assert_type(obj_explicit_new_enter, int) + +with patch.object(Decimal, "exp", new_callable=lambda: 42) as obj_explicit_new_callable_enter: + assert_type(obj_explicit_new_callable_enter, int) From cb35c541d487df5f3bd4238dd87df70b77632c0a Mon Sep 17 00:00:00 2001 From: Leonardus Chen Date: Wed, 2 Jul 2025 22:06:48 +0700 Subject: [PATCH 3/5] Fix patch and patch.object as function decorator --- stdlib/unittest/mock.pyi | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/stdlib/unittest/mock.pyi b/stdlib/unittest/mock.pyi index d4a2f72ce44c..ec5fced32ea4 100644 --- a/stdlib/unittest/mock.pyi +++ b/stdlib/unittest/mock.pyi @@ -262,7 +262,7 @@ class _patch(Generic[_T]): # 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 | AsyncMock]): +class _patch_pass_arg(_patch[_T]): @overload def __call__(self, func: _TT) -> _TT: ... # Can't use the following as ParamSpec is only allowed as last parameter: @@ -317,7 +317,7 @@ class _patcher: autospec: Any | None = ..., new_callable: Callable[..., _T], **kwargs: Any, - ) -> _patch[_T]: ... + ) -> _patch_pass_arg[_T]: ... @overload def __call__( self, @@ -329,7 +329,7 @@ class _patcher: autospec: Any | None = ..., new_callable: None = ..., **kwargs: Any, - ) -> _patch_default_new: ... + ) -> _patch_pass_arg[MagicMock | AsyncMock]: ... @overload @staticmethod def object( @@ -355,7 +355,7 @@ class _patcher: autospec: Any | None = ..., new_callable: Callable[..., _T], **kwargs: Any, - ) -> _patch[_T]: ... + ) -> _patch_pass_arg[_T]: ... @overload @staticmethod def object( @@ -368,7 +368,7 @@ class _patcher: autospec: Any | None = ..., new_callable: None = ..., **kwargs: Any, - ) -> _patch[MagicMock | AsyncMock]: ... + ) -> _patch_pass_arg[MagicMock | AsyncMock]: ... @staticmethod def multiple( target: Any, From 19deb1cd866446b1403d38b6f82305eaa45a2acf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 15:10:01 +0000 Subject: [PATCH 4/5] [pre-commit.ci] auto fixes from pre-commit.com hooks --- stdlib/@tests/test_cases/check_unittest.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/stdlib/@tests/test_cases/check_unittest.py b/stdlib/@tests/test_cases/check_unittest.py index 3bbf9822ed5e..3e1be649d79a 100644 --- a/stdlib/@tests/test_cases/check_unittest.py +++ b/stdlib/@tests/test_cases/check_unittest.py @@ -7,7 +7,7 @@ from fractions import Fraction from typing import TypedDict from typing_extensions import assert_type -from unittest.mock import MagicMock, Mock, patch, AsyncMock +from unittest.mock import AsyncMock, MagicMock, Mock, patch case = unittest.TestCase() @@ -153,10 +153,12 @@ def f_default_new(i: int, mock: MagicMock) -> str: def f_explicit_new(i: int) -> str: return "asdf" + @patch("sys.exit", new_callable=lambda: 42) def f_explicit_new_callable(i: int, new_callable_ret: int) -> str: return "asdf" + 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) @@ -164,6 +166,7 @@ def f_explicit_new_callable(i: int, new_callable_ret: int) -> str: assert_type(f_explicit_new_callable(1), str) f_explicit_new_callable("a") # Same as default new + @patch("sys.exit", new=Mock()) class TestXYZ(unittest.TestCase): attr: int = 5 @@ -191,6 +194,7 @@ def method() -> int: # Tests for mock.patch.object ### + @patch.object(Decimal, "exp") def obj_f_default_new(i: int, mock: MagicMock) -> str: return "asdf" @@ -200,10 +204,12 @@ def obj_f_default_new(i: int, mock: MagicMock) -> str: def obj_f_explicit_new(i: int) -> str: return "asdf" + @patch.object(Decimal, "exp", new_callable=lambda: 42) def obj_f_explicit_new_callable(i: int, new_callable_ret: int) -> str: return "asdf" + assert_type(obj_f_default_new(1), str) obj_f_default_new("a") # Not an error due to ParamSpec limitations assert_type(obj_f_explicit_new(1), str) @@ -212,7 +218,6 @@ def obj_f_explicit_new_callable(i: int, new_callable_ret: int) -> str: obj_f_explicit_new_callable("a") # Same as default new - with patch.object(Decimal, "exp") as obj_default_new_enter: assert_type(obj_default_new_enter, MagicMock | AsyncMock) From ade3152bf9d1a489a0fba1ff3c1b1b0c53e40d74 Mon Sep 17 00:00:00 2001 From: Leonardus Chen Date: Wed, 2 Jul 2025 22:22:27 +0700 Subject: [PATCH 5/5] Use typing.Union --- stdlib/@tests/test_cases/check_unittest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stdlib/@tests/test_cases/check_unittest.py b/stdlib/@tests/test_cases/check_unittest.py index 3e1be649d79a..d717e99a3f8d 100644 --- a/stdlib/@tests/test_cases/check_unittest.py +++ b/stdlib/@tests/test_cases/check_unittest.py @@ -5,7 +5,7 @@ from datetime import datetime, timedelta from decimal import Decimal from fractions import Fraction -from typing import TypedDict +from typing import TypedDict, Union from typing_extensions import assert_type from unittest.mock import AsyncMock, MagicMock, Mock, patch @@ -181,7 +181,7 @@ def method() -> int: with patch("sys.exit") as default_new_enter: - assert_type(default_new_enter, MagicMock | AsyncMock) + assert_type(default_new_enter, Union[MagicMock, AsyncMock]) with patch("sys.exit", new=42) as explicit_new_enter: assert_type(explicit_new_enter, int) @@ -219,7 +219,7 @@ def obj_f_explicit_new_callable(i: int, new_callable_ret: int) -> str: with patch.object(Decimal, "exp") as obj_default_new_enter: - assert_type(obj_default_new_enter, MagicMock | AsyncMock) + assert_type(obj_default_new_enter, Union[MagicMock, AsyncMock]) with patch.object(Decimal, "exp", new=42) as obj_explicit_new_enter: assert_type(obj_explicit_new_enter, int)