mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:34:57 +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
|
@ -571,10 +571,20 @@ impl<'db> ClassType<'db> {
|
|||
/// Returns the inferred type of the class member named `name`. Only bound members
|
||||
/// 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
|
||||
/// directly. Use [`ClassType::class_member`] if you require a method that will
|
||||
/// 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>(
|
||||
index_annotation: Type<'db>,
|
||||
return_annotation: Type<'db>,
|
||||
|
@ -590,7 +600,7 @@ impl<'db> ClassType<'db> {
|
|||
|
||||
let fallback_member_lookup = || {
|
||||
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))
|
||||
};
|
||||
|
||||
|
@ -840,8 +850,11 @@ impl<'db> ClassType<'db> {
|
|||
iterable_parameter,
|
||||
]);
|
||||
|
||||
let synthesized_dunder =
|
||||
CallableType::function_like(db, Signature::new(parameters, None));
|
||||
let synthesized_dunder = CallableType::function_like(
|
||||
db,
|
||||
Signature::new(parameters, None)
|
||||
.with_inherited_generic_context(inherited_generic_context),
|
||||
);
|
||||
|
||||
Place::bound(synthesized_dunder).into()
|
||||
}
|
||||
|
@ -1668,7 +1681,10 @@ impl<'db> ClassLiteral<'db> {
|
|||
}
|
||||
|
||||
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(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
inherited_generic_context: Option<GenericContext<'db>>,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
name: &str,
|
||||
) -> PlaceAndQualifiers<'db> {
|
||||
|
@ -1744,7 +1761,7 @@ impl<'db> ClassLiteral<'db> {
|
|||
// 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
|
||||
// specialization.)
|
||||
match (self.generic_context(db), ty, specialization, name) {
|
||||
match (inherited_generic_context, ty, specialization, name) {
|
||||
(
|
||||
Some(generic_context),
|
||||
Type::FunctionLiteral(function),
|
||||
|
@ -1926,7 +1943,7 @@ impl<'db> ClassLiteral<'db> {
|
|||
KnownClass::NamedTupleFallback
|
||||
.to_class_literal(db)
|
||||
.into_class_literal()?
|
||||
.own_class_member(db, None, name)
|
||||
.own_class_member(db, self.generic_context(db), None, name)
|
||||
.place
|
||||
.ignore_possibly_unbound()
|
||||
}
|
||||
|
@ -4321,7 +4338,9 @@ enum SlotsKind {
|
|||
|
||||
impl SlotsKind {
|
||||
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 {
|
||||
return Self::NotSpecified;
|
||||
};
|
||||
|
|
|
@ -360,6 +360,14 @@ impl<'db> Signature<'db> {
|
|||
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 {
|
||||
Self {
|
||||
generic_context: self.generic_context,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue