Mypy not resolving protocol class as callable in check_reverse_op_method (and possible fix)
Nessuno ha ancora preso questa issue.
- Lingua principale
- Python
- Stelle
- 20.6k
- Fork
- 3.3k
- Metriche di merge delle PR
- Metriche PR in attesa
Descrizione
Bug Report
I'm writing a custom vector class for a specific type of simulation I'm working on. This class implements, amongst other operations, __add__ and __radd__ operations, with numpy floating point arguments. However, for some reason, Mypy flags a np.floating type as not having a callable __add__ method.
To Reproduce
Minimal example.py:
from __future__ import annotations
import numpy as np
class my_float:
def __radd__(self, other: np.float32) -> my_float:
return my_float()
Running mypy example.py:
example.py:6: error: Forward operator "__add__" is not callable
Found 1 error in 1 file (checked 1 source file)
More investigation
At first, I thought this was perhaps a bug in numpy, but I am not convinced. Numpy, even though all arithmetic operators are based on ufuncs written in C, provides accurate static typing stubs for them. Mypy can resolve these correctly:
a = np.float32()
b = np.float32()
reveal_type(a + b) # Revealed type is "numpy.floating[numpy.typing._32Bit]"
I believe the problem lies in that for the np.floating class and derivatives, __add__ (and other operators) are not directly defined as (overloaded) methods, but hinted as __add__: _FloatOp[_NBit1]
Here,
class _FloatOp(Protocol[_NBit1]):
@overload
def __call__(self, __other: bool) -> floating[_NBit1]: ...
@overload
def __call__(self, __other: int) -> floating[Union[_NBit1, _NBitInt]]: ...
@overload
def __call__(self, __other: float) -> floating[Union[_NBit1, _NBitDouble]]: ...
@overload
def __call__(
self, __other: complex
) -> complexfloating[Union[_NBit1, _NBitDouble], Union[_NBit1, _NBitDouble]]: ...
@overload
def __call__(
self, __other: Union[integer[_NBit2], floating[_NBit2]]
) -> floating[Union[_NBit1, _NBit2]]: ...
I had a look in more detail in how this results in mypy not resolving the operator as callable, by cloning the current mypy master and digging into it. In the end, I believe the problem lies in the mypy method check_reverse_op_method, more specifically in the call here: https://github.com/python/mypy/blob/21897149830f36fee8d7c639b8d7596ce6e24ea4/mypy/checker.py#L1240
Instead of returning some function-like variable, it only returns an instance of type:
str(forward_type) = numpy.typing._callable._FloatOp[numpy.typing._32Bit*]
This is different than for example changing in my example.py the argument to other: float, which gives:
str(forward_type) = def (builtins.float) -> builtins.float
The forward_type is used in a call to check_overlapping_op_methods, and since there is no check for isinstance(forward_item, Instance), we end up here in the case of my original example.py:
https://github.com/python/mypy/blob/21897149830f36fee8d7c639b8d7596ce6e24ea4/mypy/checker.py#L1306
resulting in the error as in my example.
One (perhaps dirty) fix would be to add a simple test for an instance right after line 1293 in check_overlapping_op_methods and test if it is callable, and resolve that:
if isinstance(forward_item, Instance):
if forward_item.has_readable_member('__call__'):
forward_item = self.expr_checker.analyze_external_member_access('__call__',
forward_item,
context)
This fixes the issue. However, I do not know enough about mypy internals to know why the analyze_external_member_access call does not resolve the (overloaded) __call__ method in the first place. I have tried to get it to return an instance with some non-protocol classes with overloaded __call__ methods, but I cannot easily reproduce this issue.
Any other suggestions? Or is this perhaps an implementation error on the part of Numpy?
Apologies if my issue submission is too long, but since I already did some digging I thought to share it as well.
Your Environment
- Mypy version used: 0.92
- Numpy version used: 1.21.4
- Python version used: 3.9.7
Guida per i contributori
Apri la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Direzione di ricerca
Inizia con la riproduzione minima in example.py e ispeziona mypy/checker.py intorno a check_reverse_op_method e check_overlapping_op_methods, usando le posizioni del codice sorgente collegate. Confronta il percorso dell’operatore numpy basato sui protocolli con il percorso float descritto nell’issue. Il lavoro è completato quando mypy accetta l’esempio radd senza l’errore “Forward operator add is not callable” e il comportamento è adeguatamente coperto.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Valutazione
- Stack tecnologico
- numpy, python
- Ambito
- devtools, tooling
- Tipo di issue
- Bug
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Stato di attività
- Ferma
- Chiarezza
- Abbastanza chiara
- Idoneità per principianti
- 35/100