mypy icon indicating copy to clipboard operation
mypy copied to clipboard

@overload interacts badly with @property even with the `type: ignore` workaround

Open jraygauthier opened this issue 3 years ago • 2 comments

Assuming a issue.py module:

$ mypy issue.py 
issue.py:86: error: Invalid self argument "MyContainerStr" to attribute function "bounds" with type "Callable[[MyContainerBase[int]], BoundsInt]"
issue.py:86: error: Argument 1 to "check_to_str_bounds" has incompatible type "BoundsInt"; expected "BoundsStr"
Found 2 errors in 1 file (checked 1 source file)

This occurs when I attempt to overload a property. It however does not occurs for the same overload on a plain method.

issue.py is:

from typing import (ClassVar, Generic, NamedTuple, Optional, Type, TypeVar,
                    Union, overload)

BoundsInt = NamedTuple("BoundsInt", [
    ("begin", int), ("end", int)])

BoundsStr = NamedTuple("BoundsStr", [
    ("begin", str), ("end", str)])

_T = TypeVar("_T", int, str)


class MyContainerBase(Generic[_T]):
    BOUND_TYPE = None  # type: ClassVar[Optional[Type[Union[BoundsInt, BoundsStr]]]]  # noqa

    def __init__(self, begin: _T, end: _T) -> None:
        assert self.BOUND_TYPE is not None
        if isinstance(begin, int) and isinstance(end, int):
            self._bounds = BoundsInt(begin, end)  # type: Union[BoundsInt, BoundsStr]  # noqa

        assert isinstance(begin, str) and isinstance(end, str)
        self._bounds = BoundsStr(begin, end)

    @overload  # type: ignore
    @property
    def bounds(
            self: 'MyContainerBase[int]'
    ) -> BoundsInt:
        ...

    @overload  # type: ignore
    @property
    def bounds(
            self: 'MyContainerBase[str]'
    ) -> BoundsStr:
        ...

    @property
    def bounds(self) -> Union[BoundsInt, BoundsStr]:
        """Return the recurrence period's bounds."""
        return self._bounds

    @overload
    def get_bounds(
            self: 'MyContainerBase[int]'
    ) -> BoundsInt:
        ...

    @overload
    def get_bounds(
            self: 'MyContainerBase[str]'
    ) -> BoundsStr:
        ...

    def get_bounds(self) -> Union[BoundsInt, BoundsStr]:
        """Return the recurrence period's bounds."""
        return self._bounds


class MyContainerInt(MyContainerBase[int]):
    BOUND_TYPE = BoundsInt


class MyContainerStr(MyContainerBase[str]):
    BOUND_TYPE = BoundsStr


def test() -> None:
    def check_to_int_bounds(bounds: BoundsInt) -> None:
        pass

    def check_to_str_bounds(bounds: BoundsStr) -> None:
        pass

    # Typechecks as expected:
    check_to_int_bounds(MyContainerInt(1, 1).bounds)
    # As expected, if uncommented, fails with:
    #   Argument 1 to "check_to_str_bounds" has incompatible type "BoundsInt";
    #   expected "BoundsStr"
    # check_to_str_bounds(MyContainerInt(1, 1).bounds)

    # Unexpectedly, if uncommented both of those fail with:
    #   Invalid self argument "MyContainerStr" to attribute
    #   function "bounds" with type "Callable[[MyContainerBase[int]],
    #   BoundsInt]"
    check_to_str_bounds(MyContainerStr("a", "b").bounds)
    # check_to_int_bounds(MyContainerStr(1.0, 1.0).bounds)

    # We do not have the above issue with the `get_bounds` method:

    # Typechecks as expected:
    check_to_int_bounds(MyContainerInt(1, 1).get_bounds())
    # As expected, if uncommented, fails.
    # check_to_str_bounds(MyContainerInt(1, 1).get_bounds())

    # Typechecks as expected:
    check_to_str_bounds(MyContainerStr("a", "b").get_bounds())
    # As expected, if uncommented, fails.
    # check_to_int_bounds(MyContainerStr(1.0, 1.0).get_bounds())

Your Environment

  • Mypy version used: 0.790
  • Mypy command-line flags: none
  • Mypy configuration options from mypy.ini (and other config files):
[mypy]
warn_unused_ignores = True
mypy_path = distropmc
disallow_any_generics = True
  • Python version used: 3.5.9
  • Operating system and version:
$ uname -a
Linux rgauthier-precision 5.4.79 #1-NixOS SMP Sun Nov 22 09:14:12 UTC 2020 x86_64 GNU/Linux

jraygauthier avatar Jan 22 '21 04:01 jraygauthier

Slightly shorter repro:

from __future__ import annotations
from typing import overload, Generic, TypeVar

T = TypeVar('T')


class C(Generic[T]):
    @overload  # type:ignore[misc]
    @property
    def f(self: C[int]) -> int: raise NotImplementedError

    @overload  # type:ignore[misc]
    @property
    def f(self: C[str]) -> str: raise NotImplementedError

    @property  # type:ignore[misc]
    def f(self): raise NotImplementedError


c: C[str] = C()
_ = c.f  # E: Invalid self argument "C[str]" to attribute function "f" with type "Callable[[C[int]], int]"

Playground: https://mypy-play.net/?mypy=latest&python=3.11&gist=15958eb7a2388321bc5561aeebffefd7

ikonst avatar Mar 14 '23 03:03 ikonst

Here's a workaround as suggested here: https://github.com/microsoft/pyright/issues/3071#issuecomment-1043978070

from __future__ import annotations
from typing import overload, Generic, TypeVar, Any

T = TypeVar("T")


class _fDescriptor:
    @overload
    def __get__(self, instance: C[int], owner: Any) -> int:
        ...

    @overload
    def __get__(self, instance: C[str], owner: Any) -> str:
        ...

    def __get__(self, instance: object, owner: Any) -> int | str:
        raise NotImplementedError


class C(Generic[T]):
    f: _fDescriptor


c: C[str] = C()
_ = c.f
reveal_type(c.f)
ci: C[int] = C()
reveal_type(ci.f)

This will reveal the types correctly.

Dr-Irv avatar Jun 20 '23 16:06 Dr-Irv

Bump! I've just encountered the same issue. I'm not really into creating a custom descriptor.

bswck avatar Nov 30 '23 12:11 bswck