Skip to content

classmethod's in generic protocols with self-types #5872

Closed
@juanarrivillaga

Description

@juanarrivillaga
  • Are you reporting a bug, or opening a feature request?
    A potential bug

So, I've posted a corresponding question on StackOverflow, but it hasn't gotten much traction.

Here is my basic situation: I'm trying to use a Protocol that would include a couple of classmethods.
The protocol is generic too, and the signatures involves self-types:

from dataclasses import dataclass
from typing import Union, ClassVar, TypeVar, Generic, Type

from typing_extensions import Protocol


_P = TypeVar('_P', bound='PType')
class PType(Protocol):
    @classmethod
    def maximum_type_value(cls: Type[_P]) -> _P:
        ...
    @classmethod
    def minimum_type_value(cls: Type[_P]) -> _P:
        ...
    def predecessor(self: _P) -> _P:
        ...
    def successor(self: _P) -> _P:
        ...

@dataclass
class MyInteger:
    value: int
    _MAX: ClassVar[int] = 42
    _MIN: ClassVar[int] = -42
    def __post_init__(self) -> None:
        if not (self._MIN <= self.value <= self._MAX):
            msg = f"Integers must be in range [{self._MIN}, {self._MAX}]"
            raise ValueError(msg)
    @classmethod
    def maximum_type_value(cls) -> MyInteger:
        return MyInteger(cls._MAX)
    @classmethod
    def minimum_type_value(cls) -> MyInteger:
        return MyInteger(cls._MIN)
    def predecessor(self) -> MyInteger:
        return MyInteger(self.value - 1)
    def successor(self) -> MyInteger:
        return MyInteger(self.value + 1)


@dataclass
class Interval(Generic[_P]):
    low: _P
    high: _P

interval = Interval(MyInteger(1), MyInteger(2))
def foo(x: PType) -> PType:
    return x
foo(MyInteger(42))

When I try to type-check this, I get the following errors:

(py37) Juans-MacBook-Pro: juan$ mypy mcve.py
mcve.py:46: error: Value of type variable "_P" of "Interval" cannot be "MyInteger"
mcve.py:49: error: Argument 1 to "foo" has incompatible type "MyInteger"; expected "PType"
mcve.py:49: note: Following member(s) of "MyInteger" have conflicts:
mcve.py:49: note:     Expected:
mcve.py:49: note:         def maximum_type_value(cls) -> <nothing>
mcve.py:49: note:     Got:
mcve.py:49: note:         def maximum_type_value(cls) -> MyInteger
mcve.py:49: note:     Expected:
mcve.py:49: note:         def minimum_type_value(cls) -> <nothing>
mcve.py:49: note:     Got:
mcve.py:49: note:         def minimum_type_value(cls) -> MyInteger

Why are the return types registering as <nothing>? When omit the annotation for the cls argument in the protocol, I get the following error:

mcve.py:46: error: Value of type variable "_P" of "Interval" cannot be "MyInteger"
mcve.py:49: error: Argument 1 to "foo" has incompatible type "MyInteger"; expected "PType"
mcve.py:49: note: Following member(s) of "MyInteger" have conflicts:
mcve.py:49: note:     Expected:
mcve.py:49: note:         def [_P <: PType] maximum_type_value(cls) -> _P
mcve.py:49: note:     Got:
mcve.py:49: note:         def maximum_type_value(cls) -> MyInteger
mcve.py:49: note:     Expected:
mcve.py:49: note:         def [_P <: PType] minimum_type_value(cls) -> _P
mcve.py:49: note:     Got:
mcve.py:49: note:         def minimum_type_value(cls) -> MyInteger

Which, quite frankly, makes even less sense to me.

If I make the classmethods instance methods, I receive no error. I would want to avoid this, since it would require a more hefty re-design.

If I try to use a staticmethod (which would be an easier workaround) it also fails:

_P = TypeVar('_P', bound='PType')
class PType(Protocol):
    @staticmethod
    def maximum_type_value() -> _P:
        ...
    @staticmethod
    def minimum_type_value() -> _P:
        ...
    def predecessor(self: _P) -> _P:
        ...
    def successor(self: _P) -> _P:
        ...

@dataclass
class MyInteger:
    value: int
    _MAX: ClassVar[int] = 42
    _MIN: ClassVar[int] = -42
    def __post_init__(self) -> None:
        if not (self._MIN <= self.value <= self._MAX):
            msg = f"Integers must be in range [{self._MIN}, {self._MAX}]"
            raise ValueError(msg)
    @staticmethod
    def maximum_type_value() -> MyInteger:
        return MyInteger(MyInteger._MAX)
    @staticmethod
    def minimum_type_value() -> MyInteger:
        return MyInteger(MyInteger._MIN)
    def predecessor(self) -> MyInteger:
        return MyInteger(self.value - 1)
    def successor(self) -> MyInteger:
        return MyInteger(self.value + 1)

error:

mcve.py:46: error: Value of type variable "_P" of "Interval" cannot be "MyInteger"
mcve.py:49: error: Argument 1 to "foo" has incompatible type "MyInteger"; expected "PType"
mcve.py:49: note: Following member(s) of "MyInteger" have conflicts:
mcve.py:49: note:     Expected:
mcve.py:49: note:         def [_P <: PType] maximum_type_value() -> _P
mcve.py:49: note:     Got:
mcve.py:49: note:         def maximum_type_value() -> MyInteger
mcve.py:49: note:     Expected:
mcve.py:49: note:         def [_P <: PType] minimum_type_value() -> _P
mcve.py:49: note:     Got:
mcve.py:49: note:         def minimum_type_value() -> MyInteger

What is wrong with my Protocol / class definition? Am I missing something obvious?

I am using mypy version 0.620

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions