python / python/mypy

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

Abierto
#11,595 1 comentario 1 reacción 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

bug
Lenguaje dominante
Python
Estrellas
20.6k
Forks
3.3k
Métricas de merge de PR
Métricas de PR pendientes

Descripción

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

Guía de contribución

Abrir la guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Línea de trabajo

Comienza con la reproducción mínima en example.py e inspecciona en mypy/checker.py, alrededor de check_reverse_op_method y check_overlapping_op_methods, usando las ubicaciones de código fuente enlazadas. Compara la ruta del operador numpy basada en protocolos con la ruta de float descrita en el issue. Se considera terminado cuando mypy acepta el ejemplo de radd sin el error “Forward operator add is not callable” y el comportamiento está cubierto adecuadamente.

Escrito por el modelo de indexación a partir del texto del issue.

Evaluación

Stack tecnológico
numpy, python
Área
devtools, tooling
Tipo de issue
Error
Dificultad
4/5
Tiempo estimado
3-5 días
Estado de actividad
Estancado
Claridad
Bastante claro
Aptitud para principiantes
35/100

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.