mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-01 06:11:21 +00:00
[ty] Correctly instantiate generic class that inherits __init__
from generic base class (#19693)
This is subtle, and the root cause became more apparent with #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 https://github.com/astral-sh/ty/issues/588
This commit is contained in:
parent
580577e667
commit
d37911685f
4 changed files with 122 additions and 8 deletions
|
@ -329,6 +329,57 @@ class D(C[V, int]):
|
||||||
reveal_type(D(1)) # revealed: D[int]
|
reveal_type(D(1)) # revealed: D[int]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Generic class inherits `__init__` from generic base class
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Generic, TypeVar
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
U = TypeVar("U")
|
||||||
|
|
||||||
|
class C(Generic[T, U]):
|
||||||
|
def __init__(self, t: T, u: U) -> None: ...
|
||||||
|
|
||||||
|
class D(C[T, U]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
reveal_type(C(1, "str")) # revealed: C[int, str]
|
||||||
|
reveal_type(D(1, "str")) # revealed: D[int, str]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generic class inherits `__init__` from `dict`
|
||||||
|
|
||||||
|
This is a specific example of the above, since it was reported specifically by a user.
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Generic, TypeVar
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
U = TypeVar("U")
|
||||||
|
|
||||||
|
class D(dict[T, U]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
reveal_type(D(key=1)) # revealed: D[str, int]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generic class inherits `__new__` from `tuple`
|
||||||
|
|
||||||
|
(Technically, we synthesize a `__new__` method that is more precise than the one defined in typeshed
|
||||||
|
for `tuple`, so we use a different mechanism to make sure it has the right inherited generic
|
||||||
|
context. But from the user's point of view, this is another example of the above.)
|
||||||
|
|
||||||
|
```py
|
||||||
|
from typing import Generic, TypeVar
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
U = TypeVar("U")
|
||||||
|
|
||||||
|
class C(tuple[T, U]): ...
|
||||||
|
|
||||||
|
reveal_type(C((1, 2))) # revealed: C[int, int]
|
||||||
|
```
|
||||||
|
|
||||||
### `__init__` is itself generic
|
### `__init__` is itself generic
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|
|
@ -308,6 +308,42 @@ class D[V](C[V, int]):
|
||||||
reveal_type(D(1)) # revealed: D[int]
|
reveal_type(D(1)) # revealed: D[int]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Generic class inherits `__init__` from generic base class
|
||||||
|
|
||||||
|
```py
|
||||||
|
class C[T, U]:
|
||||||
|
def __init__(self, t: T, u: U) -> None: ...
|
||||||
|
|
||||||
|
class D[T, U](C[T, U]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
reveal_type(C(1, "str")) # revealed: C[int, str]
|
||||||
|
reveal_type(D(1, "str")) # revealed: D[int, str]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generic class inherits `__init__` from `dict`
|
||||||
|
|
||||||
|
This is a specific example of the above, since it was reported specifically by a user.
|
||||||
|
|
||||||
|
```py
|
||||||
|
class D[T, U](dict[T, U]):
|
||||||
|
pass
|
||||||
|
|
||||||
|
reveal_type(D(key=1)) # revealed: D[str, int]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Generic class inherits `__new__` from `tuple`
|
||||||
|
|
||||||
|
(Technically, we synthesize a `__new__` method that is more precise than the one defined in typeshed
|
||||||
|
for `tuple`, so we use a different mechanism to make sure it has the right inherited generic
|
||||||
|
context. But from the user's point of view, this is another example of the above.)
|
||||||
|
|
||||||
|
```py
|
||||||
|
class C[T, U](tuple[T, U]): ...
|
||||||
|
|
||||||
|
reveal_type(C((1, 2))) # revealed: C[int, int]
|
||||||
|
```
|
||||||
|
|
||||||
### `__init__` is itself generic
|
### `__init__` is itself generic
|
||||||
|
|
||||||
```py
|
```py
|
||||||
|
|
|
@ -571,10 +571,20 @@ impl<'db> ClassType<'db> {
|
||||||
/// Returns the inferred type of the class member named `name`. Only bound members
|
/// Returns the inferred type of the class member named `name`. Only bound members
|
||||||
/// or those marked as ClassVars are considered.
|
/// or those marked as ClassVars are considered.
|
||||||
///
|
///
|
||||||
|
/// You must provide the `inherited_generic_context` that we should use for the `__new__` or
|
||||||
|
/// `__init__` member. This is inherited from the containing class -but importantly, from the
|
||||||
|
/// class that the lookup is being performed on, and not the class containing the (possibly
|
||||||
|
/// inherited) member.
|
||||||
|
///
|
||||||
/// Returns [`Place::Unbound`] if `name` cannot be found in this class's scope
|
/// Returns [`Place::Unbound`] if `name` cannot be found in this class's scope
|
||||||
/// directly. Use [`ClassType::class_member`] if you require a method that will
|
/// directly. Use [`ClassType::class_member`] if you require a method that will
|
||||||
/// traverse through the MRO until it finds the member.
|
/// traverse through the MRO until it finds the member.
|
||||||
pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
|
pub(super) fn own_class_member(
|
||||||
|
self,
|
||||||
|
db: &'db dyn Db,
|
||||||
|
inherited_generic_context: Option<GenericContext<'db>>,
|
||||||
|
name: &str,
|
||||||
|
) -> PlaceAndQualifiers<'db> {
|
||||||
fn synthesize_getitem_overload_signature<'db>(
|
fn synthesize_getitem_overload_signature<'db>(
|
||||||
index_annotation: Type<'db>,
|
index_annotation: Type<'db>,
|
||||||
return_annotation: Type<'db>,
|
return_annotation: Type<'db>,
|
||||||
|
@ -590,7 +600,7 @@ impl<'db> ClassType<'db> {
|
||||||
|
|
||||||
let fallback_member_lookup = || {
|
let fallback_member_lookup = || {
|
||||||
class_literal
|
class_literal
|
||||||
.own_class_member(db, specialization, name)
|
.own_class_member(db, inherited_generic_context, specialization, name)
|
||||||
.map_type(|ty| ty.apply_optional_specialization(db, specialization))
|
.map_type(|ty| ty.apply_optional_specialization(db, specialization))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -840,8 +850,11 @@ impl<'db> ClassType<'db> {
|
||||||
iterable_parameter,
|
iterable_parameter,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let synthesized_dunder =
|
let synthesized_dunder = CallableType::function_like(
|
||||||
CallableType::function_like(db, Signature::new(parameters, None));
|
db,
|
||||||
|
Signature::new(parameters, None)
|
||||||
|
.with_inherited_generic_context(inherited_generic_context),
|
||||||
|
);
|
||||||
|
|
||||||
Place::bound(synthesized_dunder).into()
|
Place::bound(synthesized_dunder).into()
|
||||||
}
|
}
|
||||||
|
@ -1668,7 +1681,10 @@ impl<'db> ClassLiteral<'db> {
|
||||||
}
|
}
|
||||||
|
|
||||||
lookup_result = lookup_result.or_else(|lookup_error| {
|
lookup_result = lookup_result.or_else(|lookup_error| {
|
||||||
lookup_error.or_fall_back_to(db, class.own_class_member(db, name))
|
lookup_error.or_fall_back_to(
|
||||||
|
db,
|
||||||
|
class.own_class_member(db, self.generic_context(db), name),
|
||||||
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1716,6 +1732,7 @@ impl<'db> ClassLiteral<'db> {
|
||||||
pub(super) fn own_class_member(
|
pub(super) fn own_class_member(
|
||||||
self,
|
self,
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
|
inherited_generic_context: Option<GenericContext<'db>>,
|
||||||
specialization: Option<Specialization<'db>>,
|
specialization: Option<Specialization<'db>>,
|
||||||
name: &str,
|
name: &str,
|
||||||
) -> PlaceAndQualifiers<'db> {
|
) -> PlaceAndQualifiers<'db> {
|
||||||
|
@ -1744,7 +1761,7 @@ impl<'db> ClassLiteral<'db> {
|
||||||
// to any method with a `@classmethod` decorator. (`__init__` would remain a special
|
// to any method with a `@classmethod` decorator. (`__init__` would remain a special
|
||||||
// case, since it's an _instance_ method where we don't yet know the generic class's
|
// case, since it's an _instance_ method where we don't yet know the generic class's
|
||||||
// specialization.)
|
// specialization.)
|
||||||
match (self.generic_context(db), ty, specialization, name) {
|
match (inherited_generic_context, ty, specialization, name) {
|
||||||
(
|
(
|
||||||
Some(generic_context),
|
Some(generic_context),
|
||||||
Type::FunctionLiteral(function),
|
Type::FunctionLiteral(function),
|
||||||
|
@ -1926,7 +1943,7 @@ impl<'db> ClassLiteral<'db> {
|
||||||
KnownClass::NamedTupleFallback
|
KnownClass::NamedTupleFallback
|
||||||
.to_class_literal(db)
|
.to_class_literal(db)
|
||||||
.into_class_literal()?
|
.into_class_literal()?
|
||||||
.own_class_member(db, None, name)
|
.own_class_member(db, self.generic_context(db), None, name)
|
||||||
.place
|
.place
|
||||||
.ignore_possibly_unbound()
|
.ignore_possibly_unbound()
|
||||||
}
|
}
|
||||||
|
@ -4321,7 +4338,9 @@ enum SlotsKind {
|
||||||
|
|
||||||
impl SlotsKind {
|
impl SlotsKind {
|
||||||
fn from(db: &dyn Db, base: ClassLiteral) -> Self {
|
fn from(db: &dyn Db, base: ClassLiteral) -> Self {
|
||||||
let Place::Type(slots_ty, bound) = base.own_class_member(db, None, "__slots__").place
|
let Place::Type(slots_ty, bound) = base
|
||||||
|
.own_class_member(db, base.generic_context(db), None, "__slots__")
|
||||||
|
.place
|
||||||
else {
|
else {
|
||||||
return Self::NotSpecified;
|
return Self::NotSpecified;
|
||||||
};
|
};
|
||||||
|
|
|
@ -360,6 +360,14 @@ impl<'db> Signature<'db> {
|
||||||
Self::new(Parameters::object(db), Some(Type::Never))
|
Self::new(Parameters::object(db), Some(Type::Never))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn with_inherited_generic_context(
|
||||||
|
mut self,
|
||||||
|
inherited_generic_context: Option<GenericContext<'db>>,
|
||||||
|
) -> Self {
|
||||||
|
self.inherited_generic_context = inherited_generic_context;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
|
fn materialize(&self, db: &'db dyn Db, variance: TypeVarVariance) -> Self {
|
||||||
Self {
|
Self {
|
||||||
generic_context: self.generic_context,
|
generic_context: self.generic_context,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue