[ty] Propagate specializations to ancestor base classes (#17892)

@AlexWaygood discovered that even though we've been propagating
specializations to _parent_ base classes correctly, we haven't been
passing them on to _grandparent_ base classes:
https://github.com/astral-sh/ruff/pull/17832#issuecomment-2854360969

```py
class Bar[T]:
    x: T

class Baz[T](Bar[T]): ...
class Spam[T](Baz[T]): ...

reveal_type(Spam[int]().x) # revealed: `T`, but should be `int`
```

This PR updates the MRO machinery to apply the current specialization
when starting to iterate the MRO of each base class.
This commit is contained in:
Douglas Creager 2025-05-06 14:25:21 -04:00 committed by GitHub
parent 8152ba7cb7
commit 9085f18353
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 156 additions and 52 deletions

View file

@ -91,7 +91,7 @@ reveal_type(ListSubclass.__mro__)
class DictSubclass(typing.Dict): ...
# TODO: generic protocols
# revealed: tuple[Literal[DictSubclass], Literal[dict[Unknown, Unknown]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
# revealed: tuple[Literal[DictSubclass], Literal[dict[Unknown, Unknown]], Literal[MutableMapping[Unknown, Unknown]], Literal[Mapping[Unknown, Unknown]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
reveal_type(DictSubclass.__mro__)
class SetSubclass(typing.Set): ...
@ -113,19 +113,19 @@ reveal_type(FrozenSetSubclass.__mro__)
class ChainMapSubclass(typing.ChainMap): ...
# TODO: generic protocols
# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap[Unknown, Unknown]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
# revealed: tuple[Literal[ChainMapSubclass], Literal[ChainMap[Unknown, Unknown]], Literal[MutableMapping[Unknown, Unknown]], Literal[Mapping[Unknown, Unknown]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
reveal_type(ChainMapSubclass.__mro__)
class CounterSubclass(typing.Counter): ...
# TODO: Should be (CounterSubclass, Counter, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[CounterSubclass], Literal[Counter[Unknown]], Literal[dict[_T, int]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], typing.Generic[_T], Literal[object]]
# revealed: tuple[Literal[CounterSubclass], Literal[Counter[Unknown]], Literal[dict[Unknown, int]], Literal[MutableMapping[Unknown, int]], Literal[Mapping[Unknown, int]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], typing.Generic[_T], Literal[object]]
reveal_type(CounterSubclass.__mro__)
class DefaultDictSubclass(typing.DefaultDict): ...
# TODO: Should be (DefaultDictSubclass, defaultdict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict[Unknown, Unknown]], Literal[dict[_KT, _VT]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
# revealed: tuple[Literal[DefaultDictSubclass], Literal[defaultdict[Unknown, Unknown]], Literal[dict[Unknown, Unknown]], Literal[MutableMapping[Unknown, Unknown]], Literal[Mapping[Unknown, Unknown]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
reveal_type(DefaultDictSubclass.__mro__)
class DequeSubclass(typing.Deque): ...
@ -137,6 +137,6 @@ reveal_type(DequeSubclass.__mro__)
class OrderedDictSubclass(typing.OrderedDict): ...
# TODO: Should be (OrderedDictSubclass, OrderedDict, dict, MutableMapping, Mapping, Collection, Sized, Iterable, Container, Generic, object)
# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict[Unknown, Unknown]], Literal[dict[_KT, _VT]], Literal[MutableMapping[_KT, _VT]], Literal[Mapping[_KT, _VT]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
# revealed: tuple[Literal[OrderedDictSubclass], Literal[OrderedDict[Unknown, Unknown]], Literal[dict[Unknown, Unknown]], Literal[MutableMapping[Unknown, Unknown]], Literal[Mapping[Unknown, Unknown]], Literal[Collection], Literal[Iterable], Literal[Container], @Todo(`Protocol[]` subscript), typing.Generic, typing.Generic[_KT, _VT_co], Literal[object]]
reveal_type(OrderedDictSubclass.__mro__)
```

View file

@ -340,16 +340,27 @@ propagate through:
from typing import Generic, TypeVar
T = TypeVar("T")
U = TypeVar("U")
V = TypeVar("V")
W = TypeVar("W")
class Base(Generic[T]):
x: T | None = None
class Parent(Generic[T]):
x: T
class ExplicitlyGenericSub(Base[T], Generic[T]): ...
class ImplicitlyGenericSub(Base[T]): ...
class ExplicitlyGenericChild(Parent[U], Generic[U]): ...
class ExplicitlyGenericGrandchild(ExplicitlyGenericChild[V], Generic[V]): ...
class ExplicitlyGenericGreatgrandchild(ExplicitlyGenericGrandchild[W], Generic[W]): ...
class ImplicitlyGenericChild(Parent[U]): ...
class ImplicitlyGenericGrandchild(ImplicitlyGenericChild[V]): ...
class ImplicitlyGenericGreatgrandchild(ImplicitlyGenericGrandchild[W]): ...
reveal_type(Base[int].x) # revealed: int | None
reveal_type(ExplicitlyGenericSub[int].x) # revealed: int | None
reveal_type(ImplicitlyGenericSub[int].x) # revealed: int | None
reveal_type(Parent[int]().x) # revealed: int
reveal_type(ExplicitlyGenericChild[int]().x) # revealed: int
reveal_type(ImplicitlyGenericChild[int]().x) # revealed: int
reveal_type(ExplicitlyGenericGrandchild[int]().x) # revealed: int
reveal_type(ImplicitlyGenericGrandchild[int]().x) # revealed: int
reveal_type(ExplicitlyGenericGreatgrandchild[int]().x) # revealed: int
reveal_type(ImplicitlyGenericGreatgrandchild[int]().x) # revealed: int
```
## Generic methods

View file

@ -285,13 +285,17 @@ When a generic subclass fills its superclass's type parameter with one of its ow
propagate through:
```py
class Base[T]:
x: T | None = None
class Parent[T]:
x: T
class Sub[U](Base[U]): ...
class Child[U](Parent[U]): ...
class Grandchild[V](Child[V]): ...
class Greatgrandchild[W](Child[W]): ...
reveal_type(Base[int].x) # revealed: int | None
reveal_type(Sub[int].x) # revealed: int | None
reveal_type(Parent[int]().x) # revealed: int
reveal_type(Child[int]().x) # revealed: int
reveal_type(Grandchild[int]().x) # revealed: int
reveal_type(Greatgrandchild[int]().x) # revealed: int
```
## Generic methods