python / python/typeshed

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

Aperta
#14,339 2 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Lingua principale
Python
Stelle
5.1k
Fork
2.1k
Merge medio
1g 19h
PR unite (30g)
82

Descrizione

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 :)

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Direzione di ricerca

Esamina gli overload di unittest.mock.patch e patch.object mostrati nell'issue, quindi trova i relativi stub di typeshed e i test associati. Controlla i tipi inferiti per il patching predefinito, new, new_callable e i casi con new_callable non chiamabile; il lavoro è completo quando entrambe le API accettano solo factory chiamabili e inferiscono correttamente il tipo creato.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
python
Ambito
testing
Tipo di issue
Bug
Difficoltà
3/5
Tempo stimato
1-2 giorni
Stato di attività
Ferma
Chiarezza
Specificata chiaramente
Idoneità per principianti
48/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.