Skip to content

Fix unittest.mock.patch and unittest.mock.patch.object when new_callable is not None #14358

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 57 additions & 2 deletions stdlib/@tests/test_cases/check_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
from datetime import datetime, timedelta
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test result with old implementation

check_unittest.py:190: error: Expression is of type "MagicMock | AsyncMock", not "int"  [assert-type]
check_unittest.py:213: error: Missing positional argument "mock" in call to "obj_f_default_new"  [call-arg]
check_unittest.py:214: error: Missing positional argument "mock" in call to "obj_f_default_new"  [call-arg]
check_unittest.py:214: error: Argument 1 to "obj_f_default_new" has incompatible type "str"; expected "int"  [arg-type]
check_unittest.py:217: error: Missing positional argument "new_callable_ret" in call to "obj_f_explicit_new_callable"  [call-arg]
check_unittest.py:218: error: Missing positional argument "new_callable_ret" in call to "obj_f_explicit_new_callable"  [call-arg]
check_unittest.py:218: error: Argument 1 to "obj_f_explicit_new_callable" has incompatible type "str"; expected "int"  [arg-type]
check_unittest.py:228: error: Expression is of type "MagicMock | AsyncMock", not "int"  [assert-type]

This agrees with the upated PR description:

This PR fixes multiple issues with unittest.mock.patch and unittest.mock.patch.object

  • Both should only accept Callable | None for new_callable, currently Any | None

  • When used as context managers, both should return T for new_callable: Callable[..., T], currently they both return Mock | AsyncMock

  • When used as decorators to functions:

    • When neither new nor new_callable is given, the function signature should be updated (the mock is passed as last parameter), patch already has the correct behavior but patch.object does not
    • When new_callable is given, same as above, should update function signature. patch already has the correct behavior (by chance, explicit new_callable is not actually handled), but patch.object does not

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 MagicMock, Mock, patch
from unittest.mock import AsyncMock, MagicMock, Mock, patch

case = unittest.TestCase()

Expand Down Expand Up @@ -154,10 +154,17 @@ 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())
Expand All @@ -171,3 +178,51 @@ 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, Union[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, Union[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)
39 changes: 32 additions & 7 deletions stdlib/unittest/mock.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -315,9 +315,21 @@ class _patcher:
create: bool = ...,
spec_set: Any | None = ...,
autospec: Any | None = ...,
new_callable: Any | None = ...,
new_callable: Callable[..., _T],
**kwargs: Any,
) -> _patch_default_new: ...
) -> _patch_pass_arg[_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_pass_arg[MagicMock | AsyncMock]: ...
@overload
@staticmethod
def object(
Expand All @@ -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
Expand All @@ -341,9 +353,22 @@ class _patcher:
create: bool = ...,
spec_set: Any | None = ...,
autospec: Any | None = ...,
new_callable: Any | None = ...,
new_callable: Callable[..., _T],
**kwargs: Any,
) -> _patch_pass_arg[_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]: ...
) -> _patch_pass_arg[MagicMock | AsyncMock]: ...
@staticmethod
def multiple(
target: Any,
Expand Down