MyPy v1.t6.0 cannot infer a descriptor's type through a `Protocol` which v1.15.0 supports
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