python / python/typeshed

`unittest.mock.patch` and `unittest.mock.patch.object` are wrongly typed when new_callable is provided

Open
#14,339 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
5.1k
Forks
2.1k
Avg merge
1d 19h
Merged PRs (30d)
82

Description

Steps to reproduce

from unittest.mock import patch


class NewPrinter:
    pass


with patch("pprint.PrettyPrinter", new_callable=lambda: NewPrinter) as patched_printer:
    print(patched_printer)  # <class '__main__.NewPrinter'>
    reveal_type(patched_printer)  # "Union[unittest.mock.MagicMock, unittest.mock.AsyncMock]"

Same deal with unittest.mock.patch.object

from unittest.mock import patch
import pprint

class NewPrinter:
    ...


with patch.object(pprint, "PrettyPrinter", new_callable=lambda: NewPrinter) as patched_printer:
    print(patched_printer)  # <class '__main__.NewPrinter'>
    reveal_type(patched_printer)  # "Union[unittest.mock.MagicMock, unittest.mock.AsyncMock]"



Suggested fix

We discuss unittest.mock.patch in the following, but the same principles apply to unittest.mock.patch.object too

Current signatures:

    @overload
    def __call__(
        self,
        target: str,
        new: _T,
        spec: Any | None = ...,
        create: bool = ...,
        spec_set: Any | None = ...,
        autospec: Any | None = ...,
        new_callable: Any | None = ...,
        **kwargs: Any,
    ) -> _patch[_T]: ...
    @overload
    def __call__(
        self,
        target: str,
        *,
        spec: Any | None = ...,
        create: bool = ...,
        spec_set: Any | None = ...,
        autospec: Any | None = ...,
        new_callable: Any | None = ...,
        **kwargs: Any,
    ) -> _patch_default_new: ...

There are 2 problems here:

  • new_callable shouldn't be Any | None in the first place, it should only accept Callable| None, as the it will raise if the provided value is not a callable
    with patch("pprint.PrettyPrinter", new_callable=1) as patched_printer:  # TypeError: 'int' object is not callable
    
  • Need to differentiate the case where new_callable is provided and left as default None
    • When new_callable: Callable[..., T], returns _patch[_T]
    • When new_callable: None = ..., returns _patch_default_new

Seems like this would do the job:

    @overload
    def __call__(
        self,
        target: str,
        new: _T,
        spec: Any | None = ...,
        create: bool = ...,
        spec_set: Any | None = ...,
        autospec: Any | None = ...,
        new_callable: Callable[..., Any] | None = ...,
        **kwargs: Any,
    ) -> _patch[_T]: ...
    @overload
    def __call__(
        self,
        target: str,
        *,
        spec: Any | None = ...,
        create: bool = ...,
        spec_set: Any | None = ...,
        autospec: 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: ...

Testing results

from unittest.mock import patch


class NewPrinter:
    pass


with patch("pprint.PrettyPrinter") as patched_printer:
    print(patched_printer)  # <MagicMock name='PrettyPrinter' id='125908774402896'>
    reveal_type(patched_printer)  # MagicMock | AsyncMock


with patch("pprint.PrettyPrinter", new=NewPrinter) as patched_printer:
    print(patched_printer)  # <class '__main__.NewPrinter'>
    reveal_type(patched_printer)  # type[NewPrinter]


with patch("pprint.PrettyPrinter", new_callable=lambda: NewPrinter) as patched_printer:
    print(patched_printer)  # <class '__main__.NewPrinter'>
    reveal_type(patched_printer)  # type[NewPrinter]


# No overload variant matches for non-callable type
with patch("pprint.PrettyPrinter", new_callable="non-callable") as patched_printer:
    ...

Haven't tried unittest.mock.patch.object yet, but probably same thing


Would be great to see what the maintainers think.

I'll dig deeper and try to open a PR, maybe this weekend, will be my first PR in typeshed :)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Review the current unittest.mock.patch and patch.object overloads shown in the issue, then find their typeshed stubs and associated tests. Check inferred types for default patching, new, new_callable, and non-callable new_callable cases; done means both APIs accept only callable factories and infer the created type correctly.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
testing
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.