Mypy not resolving protocol class as callable in check_reverse_op_method (and possible fix)
Dieses Issue hat noch niemand übernommen.
- Vorherrschende Sprache
- Python
- Sterne
- 20.6k
- Forks
- 3.3k
- PR-Merge-Kennzahlen
- PR-Kennzahlen ausstehend
Beschreibung
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
Beitragsleitfaden
Erste Schritte
- Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
- Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
- Forke das Repository und arbeite in einem Branch.
- Öffne einen Pull Request, der die Issue-Nummer nennt.
Rechercherichtung
Beginne mit der minimalen example.py-Reproduktion und untersuche in mypy/checker.py anhand der verlinkten Quellstellen den Bereich um check_reverse_op_method und check_overlapping_op_methods. Vergleiche den protokollbasierten numpy-Operatorpfad mit dem im Issue beschriebenen float-Pfad. Als abgeschlossen gilt die Aufgabe, wenn mypy das radd-Beispiel ohne den Fehler „Forward operator add is not callable“ akzeptiert und das Verhalten angemessen abgedeckt ist.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Bewertung
- Tech-Stack
- numpy, python
- Bereich
- devtools, tooling
- Issue-Typ
- Bug
- Schwierigkeit
- 4/5
- Geschätzter Aufwand
- 3-5 Tage
- Aktivitätsstatus
- Veraltet
- Klarheit
- Größtenteils klar
- Anfängerfreundlichkeit
- 35/100