Skip to content

MyPy v1.t6.0 cannot infer a descriptor's type through a Protocol which v1.15.0 supports #19274

Closed as duplicate of#19054
@tdyas

Description

@tdyas

MyPy v1.t6.0 is no longer able to infer a descriptor's type through a Protocol which it could do in v1.15.0.

To Reproduce

The following code is a simplified form of the Pantsbuild option system in this file.

from dataclasses import dataclass
from typing import Any, Protocol, Union, TypeVar, Callable, Generic, cast, TYPE_CHECKING, overload

# The type of the option.
_OptT = TypeVar("_OptT")

# The type of option's default (may be _OptT or some other type like `None`)
_DefaultT = TypeVar("_DefaultT")

_SubsystemType = Any

_DynamicDefaultT = Callable[[_SubsystemType], Any]

_MaybeDynamicT = Union[_DynamicDefaultT, _DefaultT]

@dataclass
class OptionInfo:
    flag_name: tuple[str, ...] | None


class _OptionBase(Generic[_OptT, _DefaultT]):
    _flag_names: tuple[str, ...] | None
    _default: _MaybeDynamicT[_DefaultT]
    
    def __new__(
        cls,
        flag_name: str | None = None,
        *,
        default: _MaybeDynamicT[_DefaultT]
    ):
        self = super().__new__(cls)
        self._flag_names = (flag_name,) if flag_name else None
        self._default = default
        return self

    def get_option_type(self, subsystem_cls):
        return type(self).option_type

    def _convert_(self, val: Any) -> _OptT:
        return cast("_OptT", val)

    def __set_name__(self, owner, name) -> None:
        if self._flag_names is None:
            kebab_name = name.strip("_").replace("_", "-")
            self._flag_names = (f"--{kebab_name}",)

    @overload
    def __get__(self, obj: None, objtype: Any) -> OptionInfo | None: ...

    @overload
    def __get__(self, obj: object, objtype: Any) -> _OptT: ...

    def __get__(self, obj, objtype):
        assert self._flag_names is not None
        if obj is None:
            return OptionInfo(self._flag_names)
        long_name = self._flag_names[-1]
        option_value = obj.options.get(long_name[2:].replace("-", "_"))
        if option_value is None:
            return None
        return self._convert_(option_value)


_IntDefault = TypeVar("_IntDefault", int, None)

class IntOption(_OptionBase[int, _IntDefault]):
    option_type: Any = int


class ExampleOption(IntOption):
    pass


class OptionConsumer:
    example = ExampleOption(default=None)

    @property
    def options(self):
        return {"example": 30}


class HasExampleOption(Protocol):
    example: ExampleOption


oc = OptionConsumer()
oc2 = cast(HasExampleOption, oc)

if TYPE_CHECKING:
    reveal_type(oc.example)
    reveal_type(oc2.example)

(This code could probably be further simplified, but is sufficient to reproduce the bug. )

Expected Behavior

The expected behavior is that the access through the HasExampleOption protocol to the example attribute has type int. Here is the output when running v1.15.0 against the code:

src/grok.py:90: note: Revealed type is "builtins.int"
src/grok.py:91: note: Revealed type is "builtins.int"

Actual Behavior

v1.16.0 regresses and reports the type of example as the descriptor's class ExampleOption and not as int:

src/grok.py:90: note: Revealed type is "builtins.int"
src/grok.py:91: note: Revealed type is "grok.ExampleOption"

Your Environment

  • Mypy version used: v1.15.0 and v1.16.0
  • Mypy command-line flags: none
  • Mypy configuration options from mypy.ini (and other config files): none
  • Python version used: 3.11.12

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugmypy got something wrongtopic-descriptorsProperties, class vs. instance attributes

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions