python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
17.87k stars 2.74k forks source link

(regression) Interaction between `__new__` and `Generic`s #14061

Open finite-state-machine opened 1 year ago

finite-state-machine commented 1 year ago

Bug Report

Consider the following code [mypy-play.net]:

from __future__ import annotations

from typing import *

T = TypeVar('T')

class Parent(Sequence[T]):
    '''a sequence type which is implemented one of two ways,
    according to the input parameters
    '''
    def __new__(cls, model: Sequence[T]) -> Parent[T]:
        if cls is Parent:
            if len(model) % 2:
                return Odds(model)
            return Evens(model)
        return super().__new__(cls)

    _data: Sequence[T]

    @overload
    def __getitem__(self, index: int, /) -> T: ...
    @overload
    def __getitem__(self, index: slice, /) -> Sequence[T]: ...

    def __getitem__(self, index: Union[int, slice], /
            ) -> Union[Sequence[T], T]:  # pragma: no cover

        return self._data[index]

    def __len__(self) -> int:
        return len(self._data)

class Evens(Parent[T]):
    '''the implementation of 'Parent' used when the input has an even length
    '''
    def __new__(cls, model: Sequence[T]) -> Evens[T]:
        ret = super().__new__(cls, model)
                # mypy: error: Argument 1 to "__new__" of "Parent" has
                #              incompatible type "Type[Evens[T]]";
                #              expected "Type[Parent[T]]"
                #              [arg-type]
                #       error: Argument 2 to "__new__" of "Parent" has
                #              incompatible type "Sequence[T]"; expected
                #              "Sequence[T]"
                #              [arg-type]
        assert isinstance(ret, Evens)
        return ret

    def __init__(self, model: Sequence[T]):
        self._data = model[::2]

class Odds(Parent[T]):
    '''the implementation of 'Parent' used when the input has an odd length
    '''
    def __new__(cls, model: Sequence[T]) -> Odds[T]:
            # mypy: two (3) errors, analogous to those of
            #       'Evens.__new__()', above
        ret = super().__new__(cls, model)
        assert isinstance(ret, Odds)
        return ret

    def __init__(self, model: Sequence[T]):
        self._data = model[1::2]

def test_function() -> None:
    # this test passes; the code does work!

    evens = Parent(list(range(10)))
    assert isinstance(evens, Parent)
    assert isinstance(evens, Evens)
    assert list(evens) == [0, 2, 4, 6, 8]

    odds = Parent(list(range(11)))
    assert isinstance(odds, Parent)
    assert isinstance(odds, Odds)
    assert list(odds) == [1, 3, 5, 7, 9]

To Reproduce

See above.

Actual Behavior

main.py:40: error: Argument 1 to "__new__" of "Parent" has incompatible type "Type[Evens[T]]"; expected "Type[Parent[T]]"  [arg-type]
main.py:40: error: Argument 2 to "__new__" of "Parent" has incompatible type "Sequence[T]"; expected "Sequence[T]"  [arg-type]
main.py:61: error: Argument 1 to "__new__" of "Parent" has incompatible type "Type[Odds[T]]"; expected "Type[Parent[T]]"  [arg-type]
main.py:61: error: Argument 2 to "__new__" of "Parent" has incompatible type "Sequence[T]"; expected "Sequence[T]"  [arg-type]
Found 4 errors in 1 file (checked 1 source file)

Expected Behavior

There should be no errors. (In fact, this worked correctly until mypy 0.950.)

The first and third errors make little sense since Type[Evens[T]] is a special case of Type[Parent[T]].

The second and fourth errors make even less sense, since Sequence[T] is exactly the same as Sequence[T].

If there is actually some kind of problem here, the error messages need to be much more helpful than they currently are.

Your Environment

ilevkivskyi commented 1 year ago

Note that this is specific to __new__, other (explicit) class methods work correctly. I didn't check, but it may be caused by https://github.com/python/mypy/pull/12590 cc @JukkaL FWIW PR looks correct, probably it just exposes some old missing special casing for __new__. Likely should be an easy fix.

finite-state-machine commented 1 year ago

FWIW I've confirmed this issue persists on the development version of mypy (2023-06-26).

finite-state-machine commented 2 months ago

It looks like @kourbou fixed this with 1dd8e7f (#16670) when they fixed #16668:

commit 1dd8e7fe6
Author: Kouroche Bouchiat <kourbou@users.noreply.github.com>
Date:   Sun Dec 17 21:32:57 2023 +0100

    Substitute type variables in return type of static methods (#16670)

    `add_class_tvars` correctly instantiates type variables in the return
    type for class methods but not for static methods. Check if the analyzed
    member is a static method in `analyze_class_attribute_access` and
    substitute the type variable in the return type in `add_class_tvars`
    accordingly.

    Fixes #16668.

Thanks, @kourbou!

Is it worth adding a test for this case?