Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Regression since mypy 1.7 with functions that return a generic protocol #17191

Open
vxgmichel opened this issue Apr 29, 2024 · 3 comments
Open
Labels
bug mypy got something wrong

Comments

@vxgmichel
Copy link

vxgmichel commented Apr 29, 2024

EDIT: I found a simpler example reproducing the regression, see below

The following code used to pass with mypy 1.6.1

from typing import (
    Protocol,
    TypeVar,
    Callable,
    Concatenate,
    ParamSpec,
    reveal_type,
)


X = TypeVar("X")
Y = TypeVar("Y")
P = ParamSpec("P")
I = TypeVar("I", contravariant=True)
O = TypeVar("O", covariant=True)


# A callable converting I to O with parameters P
ConvertorCallable = Callable[Concatenate[I, P], O]


# An object that can convert I to O with parameters P
class ConvertorProtocol(Protocol[I, P, O]):
    def convert(self, __source: I, *args: P.args, **kwargs: P.kwargs) -> O:
        ...


# Decorator to convert a callable to a convertor
def convertor(
    func: ConvertorCallable[X, P, Y],
) -> ConvertorProtocol[X, P, Y]:
    class Convertor:
        def convert(self, source: X, /, *args: P.args, **kwargs: P.kwargs) -> Y:
            return func(source, *args, **kwargs)

    return Convertor()


# A convertor that converts X to a list of X
@convertor
def as_list(source: X, repeat: int = 1) -> list[X]:
    return [source] * repeat


if __name__ == "__main__":
    result = as_list.convert(1, repeat=3)
    reveal_type(result)
    print(result)

However, it no longer passes with mypy 1.7.0 and later:

error: Argument 1 to "convert" of "ConvertorProtocol" has incompatible type "int"; expected Never  [arg-type]

Try it in the playground.

Note that this sample works in the pyright playground.

@vxgmichel vxgmichel added the bug mypy got something wrong label Apr 29, 2024
@vxgmichel
Copy link
Author

vxgmichel commented May 2, 2024

To add a bit more information, the same example works fine if the I type is included within the P param spec (see that in the playground).

However, being able to extract a type from the param spec as seen in the first code sample is really useful. In particular, the type of the first argument might appear in other methods of the protocol, for instance:

# An object that can convert I to O with parameters P
class ConvertorProtocol(Protocol[I, P, O]):
    def convert(self, __source: I, *args: P.args, **kwargs: P.kwargs) -> O:
        ...

    def prepare(self, *args: P.args, **kwargs: P.kwargs) -> Callable[[I], O]:
        ...

@vxgmichel
Copy link
Author

I just came up with a simpler example that can reproduce the regression, without Protocol nor decorators:

from typing import (
    Generic,
    TypeVar,
    Callable,
    Concatenate,
    ParamSpec,
    reveal_type,
)
from dataclasses import dataclass


X = TypeVar("X")
P = ParamSpec("P")
I = TypeVar("I", contravariant=True)
O = TypeVar("O", covariant=True)


@dataclass
class Convertor(Generic[I, P, O]):
    convert: Callable[Concatenate[I, P], O]


def as_list(source: X, repeat: int = 1) -> list[X]:
    return [source] * repeat


as_list_convertor = Convertor(convert=as_list)


if __name__ == "__main__":
    reveal_type(as_list_convertor.convert)
    result = as_list_convertor.convert(1, repeat=3)
    reveal_type(result)
    assert result == [1, 1, 1]

You can see in the playground that it passes with mypy 1.6 but starts failing with mypy 1.7.

@randolf-scholz
Copy link
Contributor

randolf-scholz commented Jul 20, 2024

I am having the same issue, mypy infers Never for the type-variable. Interestingly, binding the expression first causes it to infer Any instead. Maybe this could have similar causes as microsoft/pyright#8165?

from typing import Protocol, Sequence, reveal_type, Any

class ClassDecorator[T, **P](Protocol):
    def __call__(self, cls: type[T], /, *args: P.args, **kwargs: P.kwargs) -> type[T]: ...
    
def as_class_decorator[T, **P](x: ClassDecorator[T, P]) -> ClassDecorator[T, P]:
    return x

def pprint_sequence[S: Sequence](cls: type[S], /, **kwds: Any) -> type[S]:
    return cls

reveal_type(as_class_decorator(pprint_sequence))
# >>> mypy: ClassDecorator[Never, [**kwds: Any]]
# >>> pyright: ClassDecorator[S@pprint_sequence, (**kwds: Any)]

# if we assign to a variable first, we at least don't get `Never`
fn = as_class_decorator(pprint_sequence)
reveal_type(fn)
# >>> mypy: ClassDecorator[Any, [**kwds: Any]]
# >>> pyright: ClassDecorator[S@pprint_sequence, (**kwds: Any)]

[mypy-playground], [pyright playground]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug mypy got something wrong
Projects
None yet
Development

No branches or pull requests

2 participants