Skip to content

Commit 56180bc

Browse files
[ty] Specialize instance members once (#29043)
## Summary We apply a generic class's specialization once when looking up an instance attribute. MRO lookup already specializes each member in its defining class, but the outer lookup applied the specialization again. When type arguments refer to the class's own parameters, that could turn `list[T]` into `list[list[T]]` or undo a swap of `T` and `U`. Remove the redundant specialization and cover nested and swapped type arguments with both legacy and PEP 695 syntax.
1 parent aa9a1ff commit 56180bc

3 files changed

Lines changed: 48 additions & 5 deletions

File tree

‎crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2448,6 +2448,30 @@ class WithOverloadedMethod(Generic[T]):
24482448
reveal_type(WithOverloadedMethod[int].method)
24492449
```
24502450

2451+
## Instance attributes with substituted type variables
2452+
2453+
Instance attributes are specialized once, even when the type arguments refer to the class's own type
2454+
parameters.
2455+
2456+
```py
2457+
from typing import Generic, TypeVar
2458+
2459+
T = TypeVar("T")
2460+
U = TypeVar("U")
2461+
2462+
class Pair(Generic[T, U]):
2463+
def __init__(self, first: T, second: U):
2464+
self.first: T = first
2465+
self.second: U = second
2466+
2467+
def swapped(self, other: "Pair[U, T]"):
2468+
reveal_type(other.first) # revealed: U@Pair
2469+
reveal_type(other.second) # revealed: T@Pair
2470+
2471+
def nested(self, other: "Pair[list[T], U]"):
2472+
reveal_type(other.first) # revealed: list[T@Pair]
2473+
```
2474+
24512475
## Materialized `TypeIs` return types
24522476

24532477
A generic `TypeIs` in the return type of a class method is materialized along with the outer class:

‎crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1733,6 +1733,25 @@ class WithOverloadedMethod[T]:
17331733
reveal_type(WithOverloadedMethod[int].method)
17341734
```
17351735

1736+
## Instance attributes with substituted type variables
1737+
1738+
Instance attributes are specialized once, even when the type arguments refer to the class's own type
1739+
parameters.
1740+
1741+
```py
1742+
class Pair[T, U]:
1743+
def __init__(self, first: T, second: U):
1744+
self.first: T = first
1745+
self.second: U = second
1746+
1747+
def swapped(self, other: "Pair[U, T]"):
1748+
reveal_type(other.first) # revealed: U@Pair
1749+
reveal_type(other.second) # revealed: T@Pair
1750+
1751+
def nested(self, other: "Pair[list[T], U]"):
1752+
reveal_type(other.first) # revealed: list[T@Pair]
1753+
```
1754+
17361755
## Materialized `TypeIs` return types
17371756

17381757
A generic `TypeIs` in the return type of a class method is materialized along with the outer class:

‎crates/ty_python_semantic/src/types/class.rs‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2274,11 +2274,11 @@ impl<'db> ClassType<'db> {
22742274
return Place::Undefined.into();
22752275
}
22762276

2277-
class_literal
2278-
.instance_member(db, env, specialization, name)
2279-
.map_type(|ty| {
2280-
ty.apply_optional_owner_specialization_to_member(db, specialization)
2281-
})
2277+
// MRO lookup already specializes each member in the class that defines it.
2278+
// Applying the receiver's specialization again would also substitute type
2279+
// variables inside the replacement types. For example, applying `T -> list[T]`
2280+
// twice changes an attribute's type from `list[T]` to `list[list[T]]`.
2281+
class_literal.instance_member(db, env, specialization, name)
22822282
}
22832283
}
22842284
}

0 commit comments

Comments
 (0)