Skip to content

Commit d379116

Browse files
authored
[ty] Correctly instantiate generic class that inherits __init__ from generic base class (astral-sh#19693)
This is subtle, and the root cause became more apparent with astral-sh#19604, since we now have many more cases of superclasses and subclasses using different typevars. The issue is easiest to see in the following: ```py class C[T]: def __init__(self, t: T) -> None: ... class D[U](C[T]): pass reveal_type(C(1)) # revealed: C[int] reveal_type(D(1)) # should be: D[int] ``` When instantiating a generic class, the `__init__` method inherits the generic context of that class. This lets our call binding machinery infer a specialization for that context. Prior to this PR, the instantiation of `C` worked just fine. Its `__init__` method would inherit the `[T]` generic context, and we would infer `{T = int}` as the specialization based on the argument parameters. It didn't work for `D`. The issue is that the `__init__` method was inheriting the generic context of the class where `__init__` was defined (here, `C` and `[T]`). At the call site, we would then infer `{T = int}` as the specialization — but that wouldn't help us specialize `D[U]`, since `D` does not have `T` in its generic context! Instead, the `__init__` method should inherit the generic context of the class that we are performing the lookup on (here, `D` and `[U]`). That lets us correctly infer `{U = int}` as the specialization, which we can successfully apply to `D[U]`. (Note that `__init__` refers to `C`'s typevars in its signature, but that's okay; our member lookup logic already applies the `T = U` specialization when returning a member of `C` while performing a lookup on `D`, transforming its signature from `(Self, T) -> None` to `(Self, U) -> None`.) Closes astral-sh/ty#588
1 parent 580577e commit d379116

File tree

4 files changed

+122
-8
lines changed

4 files changed

+122
-8
lines changed

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,57 @@ class D(C[V, int]):
329329
reveal_type(D(1)) # revealed: D[int]
330330
```
331331

332+
### Generic class inherits `__init__` from generic base class
333+
334+
```py
335+
from typing import Generic, TypeVar
336+
337+
T = TypeVar("T")
338+
U = TypeVar("U")
339+
340+
class C(Generic[T, U]):
341+
def __init__(self, t: T, u: U) -> None: ...
342+
343+
class D(C[T, U]):
344+
pass
345+
346+
reveal_type(C(1, "str")) # revealed: C[int, str]
347+
reveal_type(D(1, "str")) # revealed: D[int, str]
348+
```
349+
350+
### Generic class inherits `__init__` from `dict`
351+
352+
This is a specific example of the above, since it was reported specifically by a user.
353+
354+
```py
355+
from typing import Generic, TypeVar
356+
357+
T = TypeVar("T")
358+
U = TypeVar("U")
359+
360+
class D(dict[T, U]):
361+
pass
362+
363+
reveal_type(D(key=1)) # revealed: D[str, int]
364+
```
365+
366+
### Generic class inherits `__new__` from `tuple`
367+
368+
(Technically, we synthesize a `__new__` method that is more precise than the one defined in typeshed
369+
for `tuple`, so we use a different mechanism to make sure it has the right inherited generic
370+
context. But from the user's point of view, this is another example of the above.)
371+
372+
```py
373+
from typing import Generic, TypeVar
374+
375+
T = TypeVar("T")
376+
U = TypeVar("U")
377+
378+
class C(tuple[T, U]): ...
379+
380+
reveal_type(C((1, 2))) # revealed: C[int, int]
381+
```
382+
332383
### `__init__` is itself generic
333384

334385
```py

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,42 @@ class D[V](C[V, int]):
308308
reveal_type(D(1)) # revealed: D[int]
309309
```
310310

311+
### Generic class inherits `__init__` from generic base class
312+
313+
```py
314+
class C[T, U]:
315+
def __init__(self, t: T, u: U) -> None: ...
316+
317+
class D[T, U](C[T, U]):
318+
pass
319+
320+
reveal_type(C(1, "str")) # revealed: C[int, str]
321+
reveal_type(D(1, "str")) # revealed: D[int, str]
322+
```
323+
324+
### Generic class inherits `__init__` from `dict`
325+
326+
This is a specific example of the above, since it was reported specifically by a user.
327+
328+
```py
329+
class D[T, U](dict[T, U]):
330+
pass
331+
332+
reveal_type(D(key=1)) # revealed: D[str, int]
333+
```
334+
335+
### Generic class inherits `__new__` from `tuple`
336+
337+
(Technically, we synthesize a `__new__` method that is more precise than the one defined in typeshed
338+
for `tuple`, so we use a different mechanism to make sure it has the right inherited generic
339+
context. But from the user's point of view, this is another example of the above.)
340+
341+
```py
342+
class C[T, U](tuple[T, U]): ...
343+
344+
reveal_type(C((1, 2))) # revealed: C[int, int]
345+
```
346+
311347
### `__init__` is itself generic
312348

313349
```py

crates/ty_python_semantic/src/types/class.rs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -571,10 +571,20 @@ impl<'db> ClassType<'db> {
571571
/// Returns the inferred type of the class member named `name`. Only bound members
572572
/// or those marked as ClassVars are considered.
573573
///
574+
/// You must provide the `inherited_generic_context` that we should use for the `__new__` or
575+
/// `__init__` member. This is inherited from the containing class -­but importantly, from the
576+
/// class that the lookup is being performed on, and not the class containing the (possibly
577+
/// inherited) member.
578+
///
574579
/// Returns [`Place::Unbound`] if `name` cannot be found in this class's scope
575580
/// directly. Use [`ClassType::class_member`] if you require a method that will
576581
/// traverse through the MRO until it finds the member.
577-
pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
582+
pub(super) fn own_class_member(
583+
self,
584+
db: &'db dyn Db,
585+
inherited_generic_context: Option<GenericContext<'db>>,
586+
name: &str,
587+
) -> PlaceAndQualifiers<'db> {
578588
fn synthesize_getitem_overload_signature<'db>(
579589
index_annotation: Type<'db>,
580590
return_annotation: Type<'db>,
@@ -590,7 +600,7 @@ impl<'db> ClassType<'db> {
590600

591601
let fallback_member_lookup = || {
592602
class_literal
593-
.own_class_member(db, specialization, name)
603+
.own_class_member(db, inherited_generic_context, specialization, name)
594604
.map_type(|ty| ty.apply_optional_specialization(db, specialization))
595605
};
596606

@@ -840,8 +850,11 @@ impl<'db> ClassType<'db> {
840850
iterable_parameter,
841851
]);
842852

843-
let synthesized_dunder =
844-
CallableType::function_like(db, Signature::new(parameters, None));
853+
let synthesized_dunder = CallableType::function_like(
854+
db,
855+
Signature::new(parameters, None)
856+
.with_inherited_generic_context(inherited_generic_context),
857+
);
845858

846859
Place::bound(synthesized_dunder).into()
847860
}
@@ -1668,7 +1681,10 @@ impl<'db> ClassLiteral<'db> {
16681681
}
16691682

16701683
lookup_result = lookup_result.or_else(|lookup_error| {
1671-
lookup_error.or_fall_back_to(db, class.own_class_member(db, name))
1684+
lookup_error.or_fall_back_to(
1685+
db,
1686+
class.own_class_member(db, self.generic_context(db), name),
1687+
)
16721688
});
16731689
}
16741690
}
@@ -1716,6 +1732,7 @@ impl<'db> ClassLiteral<'db> {
17161732
pub(super) fn own_class_member(
17171733
self,
17181734
db: &'db dyn Db,
1735+
inherited_generic_context: Option<GenericContext<'db>>,
17191736
specialization: Option<Specialization<'db>>,
17201737
name: &str,
17211738
) -> PlaceAndQualifiers<'db> {
@@ -1744,7 +1761,7 @@ impl<'db> ClassLiteral<'db> {
17441761
// to any method with a `@classmethod` decorator. (`__init__` would remain a special
17451762
// case, since it's an _instance_ method where we don't yet know the generic class's
17461763
// specialization.)
1747-
match (self.generic_context(db), ty, specialization, name) {
1764+
match (inherited_generic_context, ty, specialization, name) {
17481765
(
17491766
Some(generic_context),
17501767
Type::FunctionLiteral(function),
@@ -1926,7 +1943,7 @@ impl<'db> ClassLiteral<'db> {
19261943
KnownClass::NamedTupleFallback
19271944
.to_class_literal(db)
19281945
.into_class_literal()?
1929-
.own_class_member(db, None, name)
1946+
.own_class_member(db, self.generic_context(db), None, name)
19301947
.place
19311948
.ignore_possibly_unbound()
19321949
}
@@ -4321,7 +4338,9 @@ enum SlotsKind {
43214338

43224339
impl SlotsKind {
43234340
fn from(db: &dyn Db, base: ClassLiteral) -> Self {
4324-
let Place::Type(slots_ty, bound) = base.own_class_member(db, None, "__slots__").place
4341+
let Place::Type(slots_ty, bound) = base
4342+
.own_class_member(db, base.generic_context(db), None, "__slots__")
4343+
.place
43254344
else {
43264345
return Self::NotSpecified;
43274346
};

crates/ty_python_semantic/src/types/signatures.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,14 @@ impl<'db> Signature<'db> {
360360
Self::new(Parameters::object(db), Some(Type::Never))
361361
}
362362

363+
pub(crate) fn with_inherited_generic_context(
364+
mut self,
365+
inherited_generic_context: Option<GenericContext<'db>>,
366+
) -> Self {
367+
self.inherited_generic_context = inherited_generic_context;
368+
self
369+
}
370+
363371
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
364372
Self {
365373
generic_context: self.generic_context,

0 commit comments

Comments
 (0)