microsoft / pyright

Static Type Checker for Python
Other
12.7k stars 1.35k forks source link

False positive type error due to protocol insufficiently widened in function call #8301

Open robert-bn opened 2 weeks ago

robert-bn commented 2 weeks ago

Consider the following code:

from typing import Protocol

class Getter[T,U](Protocol):
    def __call__(self, x: T, /) -> U: ...

class PolymorphicListItemGetter(Protocol):
    def __call__[T](self, l: list[T], /) -> T: ...

def compose_getters[T,U,V](get1: Getter[T,U], get2: Getter[U,V]) -> Getter[T,V]: ...

class HasFoo(Protocol):
    @property
    def foo(self) -> int:
        ...

def get_foo(x: HasFoo) -> int:
    ...

def upcast(x: PolymorphicListItemGetter) -> Getter[list[HasFoo], HasFoo]:
    return x

def example(poly_getter: PolymorphicListItemGetter):
    compose_getters(poly_getter, get_foo)
    compose_getters(upcast(poly_getter), get_foo)

pyright doesn't raise an error in the definition of upcast, indicating PolymorphicListItemGetter can be widened to Getter[list[HasFoo], HasFoo], but when it is passed as an argument directly to compose_getters pyright is unable to widen its type.

I would expect there to be no type error, as PolymorphicListItemGetter is a subtype of Getter[list[HasFoo], HasFoo].

The error raised by pyright is

    Type "(x: HasFoo) -> int" is incompatible with type "(x: T@__call__, /) -> int"
      Parameter 1: type "T@__call__" is incompatible with type "HasFoo"
        "object*" is incompatible with protocol "HasFoo"
          "foo" is not present (reportArgumentType)

Tested with pyright cli version 1.1.370

erictraut commented 2 weeks ago

Thanks for the issue. I agree this looks like a bug in pyright's constraint solver. I'll investigate further.

In the meantime, you can work around the issue by making the type parameter used in PolymorphicListItemGetter a class-scoped type variable rather than a method-scoped type variable.

class PolymorphicListItemGetter[T](Protocol):
    def __call__(self, l: list[T], /) -> T: ...