python / python/mypy

Mypy not resolving protocol class as callable in check_reverse_op_method (and possible fix)

Offen
#11,595 1 Kommentar 1 Reaktion 0 zugewiesene Personen Auf GitHub ansehen

Dieses Issue hat noch niemand übernommen.

bug
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

Beitragsleitfaden öffnen

Erste Schritte

  1. Lies das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreib ins Issue, dass du es übernimmst — das erspart doppelte Arbeit.
  3. Forke das Repository und arbeite in einem Branch.
  4. Ö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

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.