pyright icon indicating copy to clipboard operation
pyright copied to clipboard

Type checking problem with generic `__init__()` of generic class.

Open Luffbee opened this issue 11 months ago • 3 comments

Describe the bug I'm trying to write a generic container class Container[T], and support method do_map(self, func: Callable[[T], W]) -> Container[W] that do a transform like map(). For some reasons, I want the the __init__ to accept a list[U] and a function Callable[[U], T], and call map() in the __init__. Then the do_map() method just need to call the construction of the new container with type Container[W]. However, the following code (Container0) is rejected by pyright, but mypy accepts it. Although I can bypass it by a helper function (Container1 with do_map_helper), it's not convenient, and seems like a bug.

Code or Screenshots

# container.py
from typing import Callable, Generic, TypeVar

T = TypeVar("T")
U = TypeVar("U")
W = TypeVar("W")


class Container0(Generic[T]):
    def __init__(self, vals: list[U], func: Callable[[U], T]):
        self._vals = list(map(func, vals))

    def do_map(self, func: Callable[[T], W]) -> "Container0[W]":
        return Container0(self._vals, func)


class Container1(Generic[T]):
    def __init__(self, vals: list[U], func: Callable[[U], T]):
        self._vals = list(map(func, vals))

    def do_map(self, func: Callable[[T], W]) -> "Container1[W]":
        return do_map_helper(self, func)


def do_map_helper(c: Container1[T], func: Callable[[T], W]) -> Container1[W]:
    return Container1(c._vals, func)

image image

The type info of the Container0 in the do_map() function seems right, but the type check of argument and parameter report error: image

If your code relies on symbols that are imported from a third-party library, include the associated import statements and specify which versions of those libraries you have installed.

VS Code extension or command-line This problem can be reproduced by both Pylance and pyright command-line.

$ pyright --version; mypy --version
pyright 1.1.351
mypy 1.8.0 (compiled: yes)

image

Luffbee avatar Feb 29 '24 08:02 Luffbee

I'm experiencing a similar issue, not sure if it's the same issue:

1. .command

from forge.agent.protocols import CommandProvider

P = ParamSpec("P")
CO = TypeVar("CO")  # command output

_CP = TypeVar("_CP", bound=CommandProvider)


class Command(Generic[P, CO]):

    def __init__(
        self,
        method: Callable[P, CO] | Callable[Concatenate[_CP, P], CO],
    ):
        # Methods technically have a `self` parameter, but we can ignore that
        # since Python passes it internally.
        self.method = cast(Callable[P, CO], method)

so far, so good

2. .decorator

from .command import CO, Command, P

_CP = TypeVar("_CP", bound=CommandProvider)

def command() -> Callable[[Callable[P, CO] | Callable[Concatenate[_CP, P], CO]], Command[P, CO]]:

    def decorator(func: Callable[P, CO] | Callable[Concatenate[_CP, P], CO]) -> Command[P, CO]:
        # ...
        return Command(
            method=func,  # Error: Argument of type "((**P@decorator) -> CO@decorator) | ((_CP@decorator, **P@decorator) -> CO@decorator)" cannot be assigned to parameter "method" of type "((**P@Command) -> CO@Command) | ((_CP@__init__, **P@Command) -> CO@Command)" in function "__init__"
        )

    return decorator
    # Error: Expression of type "(func: ((**P@decorator) -> CO@decorator) | ((_CP@decorator, **P@decorator) -> CO@decorator)) -> Command[P@decorator, CO@decorator]" cannot be assigned to return type "(((**P@command) -> CO@command) | ((_CP@command, **P@command) -> CO@command)) -> Command[P@command, CO@command]"
    #   Type "(func: ((**P@decorator) -> CO@decorator) | ((_CP@decorator, **P@decorator) -> CO@decorator)) -> Command[P@decorator, CO@decorator]" cannot be assigned to type "(((**P@command) -> CO@command) | ((_CP@command, **P@command) -> CO@command)) -> Command[P@command, CO@command]"
    #     Parameter 1: type "((**P@command) -> CO@command) | ((_CP@command, **P@command) -> CO@command)" cannot be assigned to type "((**P@decorator) -> CO@decorator) | ((_CP@decorator, **P@decorator) -> CO@decorator)"

Despite the type vars being directly imported and the type unions being the same, Pyright won't have it: Argument of type "((**P@decorator) -> CO@decorator) | ((_CP@decorator, **P@decorator) -> CO@decorator)" cannot be assigned to parameter "method" of type "((**P@Command) -> CO@Command) | ((_CP@__init__, **P@Command) -> CO@Command)" in function "__init__" and Parameter 1: type "((**P@command) -> CO@command) | ((_CP@command, **P@command) -> CO@command)" cannot be assigned to type "((**P@decorator) -> CO@decorator) | ((_CP@decorator, **P@decorator) -> CO@decorator)"

Pyright doesn't have any issues with the above example if I remove either side of the union in all occurrences of Callable[P, CO] | Callable[Concatenate[_CP, P], CO].

I can also make the first error (on method=func) disappear like this:

    return Command[P, CO](
        method=func,
    )

but that doesn't resolve the error on return decorator

Pwuts avatar Jun 08 '24 10:06 Pwuts

@Pwuts, I don't think your issue is the same. If you think you've found a different bug, please file a separate bug report. I'm not able to repro any errors with the code you've provided. However, I will note that your use of _CP in the union is problematic because it can go unsolved (as indicated in the warning).

erictraut avatar Jun 08 '24 15:06 erictraut

@erictraut, I may have stripped off a bit too much code in an effort to make a minimum example. I'll test again and submit a new issue.

You're right about the warning that _CP is only used once. Replacing _CP by CommandProvider doesn't make the other errors go away though.

Pwuts avatar Jun 08 '24 16:06 Pwuts

This is addressed in pyright 1.1.373.

erictraut avatar Jul 24 '24 00:07 erictraut