python / mypy

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

Super in a generic classmethod gives error #9282

Closed giladsheffer closed 2 years ago

giladsheffer commented 4 years ago
from typing import TypeVar, Type

T = TypeVar("T", bound="A")

class A:
    @classmethod
    def foo(cls: Type[T]) -> T:
        print(cls.__qualname__)
        return cls()

class B(A):
    @classmethod
    def foo(cls: Type[T]) -> T:
        return super().foo()

B.foo()
crbunney commented 3 years ago

I've encountered this issue also.

In my case, changing my equivalent of B provided a workaround. It looks like this:

class B(A):
    @classmethod
    def foo(cls) -> "B":
        return super().foo()

This gets rid of the warnings I was getting, but I suspect it won't play nice with any further subclasses. I do want to be able to subclass B further, so is there a better way of working around this without resorting to #type: ignore?

g3n35i5 commented 3 years ago

I think this is still an issue in mypy v0.910

T = TypeVar("T", bound="ConfigFile")  # pylint: disable=invalid-name

class ConfigFile:
    def __init__(self, content: str) -> None:
        self._content: str = content

    @classmethod
    def from_file(cls: Type[T], file_path: str) -> T:
        with open(file=file_path, mode="r") as _file_handler:
            return cls(content=_file_handler.read())

class FooConfigFile(ConfigFile):
    @classmethod
    def from_file(cls: Type[T], file_path="/tmp/foo") -> T:
        return super().from_file(file_path=file_path)

results in

error: Argument 2 for "super" not an instance of argument 1

Selection_20210811_04

christianbundy commented 2 years ago

Confirming that this still happens in Mypy 0.930: https://mypy-play.net/?mypy=latest&python=3.10&gist=94cf4781e3ebc86b2d4ea9ef4cf7f2e7

allista commented 2 years ago

0.942 still has this issue

    @classmethod
    def _extract(cls: Type[RegexExtractorType], match: Match) -> Dict[str, Any]:
        kwargs = super()._extract(match)

error: Argument 2 for "super" not an instance of argument 1

AlexWaygood commented 2 years ago

A workaround is just to use a second TypeVar for the subclass:

from typing import TypeVar, Type

T = TypeVar("T", bound="A")

class A:
    @classmethod
    def foo(cls: Type[T]) -> T:
        print(cls.__qualname__)
        return cls()

T2 = TypeVar("T2", bound="B")

class B(A):
    @classmethod
    def foo(cls: Type[T2]) -> T2:
        return super().foo()

B.foo()
posita commented 2 years ago

@AlexWaygood, that's cool, but it doesn't work for __new__/metaclasses:

# test_case.py
from typing import Any, Mapping, Tuple, Type, TypeVar

_TT = TypeVar("_TT", bound=type)

class Foo(type):
    def __new__(
        mcls: Type[_TT],
        name: str,
        bases: Tuple[Type, ...],
        namespace: Mapping[str, Any],
        **kw: Any,
    ) -> _TT:
        return super().__new__(mcls, name, bases, namespace, **kw)  # <-- still errors
% mypy --version
mypy 0.941
% python --version
Python 3.9.10
% mypy --config=/dev/null test_case.py
/dev/null: No [mypy] section in config file
test_case.py:15: error: Argument 2 for "super" not an instance of argument 1
Found 1 error in 1 file (checked 1 source file)
AlexWaygood commented 2 years ago

@posita, there's two issues in your example:

  1. The stub for type in typeshed says that the namespace argument in type.__new__ has to be dict[str, Any]. But the third parameter of Foo.__new__ is annotated with Mapping[str, Any]. Mapping[str, Any] is not a subtype of dict[str, Any], so passing an object of type Mapping[str, Any] to the third argument of super().__new__() is unsafe.

  2. Your _TT TypeVar needs to be bound to a forward reference "Foo" rather than bound to type.

If I modify your snippet to the following:

from typing import Any, Dict, Mapping, Tuple, Type, TypeVar

_TT = TypeVar("_TT", bound="Foo")

class Foo(type):
    def __new__(
        mcls: Type[_TT],
        name: str,
        bases: Tuple[type, ...],
        namespace: Dict[str, Any],
        **kw: Any,
    ) -> _TT:
        return super().__new__(mcls, name, bases, namespace, **kw)

Then mypy is happy with it 🙂

(I'm not saying this is ideal — it's just a workaround.)

AlexWaygood commented 2 years ago

The stub for typeshed is correct that the namespace argument to type.__new__ has to be a dict rather than any old Mapping, FWIW:


>>> from collections import UserDict
>>> type.__new__(type, 'Foo', (), UserDict())
Traceback (most recent call last):
  File "<string>", line 1, in <module>
TypeError: type.__new__() argument 3 must be dict, not UserDict
>>> type.__new__(type, 'Foo', (), {})
<class '__main__.Foo'>
posita commented 2 years ago

Ah! Brilliant! My bad. Thanks for the schoolin'! 🙇

bharel commented 2 years ago

It's more of an inconvenience than a bug. Technically mypy is correct, Type[T] bound to a higher class in the hierarchy is not a subclass of the current class. Defining a new type for every subclass though is quite annoying, so there should be some sort of a solution.