Closed as duplicate of#19054
Closed as duplicate of#19054
Description
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