[ty] Make tuple subclass constructors sound (#19469)

This commit is contained in:
Alex Waygood 2025-07-21 22:25:11 +01:00 committed by GitHub
parent fcdffe4ac9
commit cb5a9ff8dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 201 additions and 66 deletions

View file

@ -4427,31 +4427,14 @@ impl<'db> Type<'db> {
.into()
}
Type::GenericAlias(alias) => {
let instantiated = Type::instance(db, ClassType::from(alias));
let parameters = if alias.origin(db).is_known(db, KnownClass::Tuple) {
// ```py
// class tuple:
// @overload
// def __new__(cls: type[tuple[()]], iterable: tuple[()] = ()) -> tuple[()]: ...
// @overload
// def __new__[T](cls: type[tuple[T, ...]], iterable: tuple[T, ...]) -> tuple[T, ...]: ...
// ```
let spec = alias.specialization(db).tuple(db);
let mut parameter =
Parameter::positional_only(Some(Name::new_static("iterable")))
.with_annotated_type(instantiated);
if matches!(spec.len().maximum(), Some(0)) {
parameter = parameter.with_default_type(TupleType::empty(db));
}
Parameters::new([parameter])
} else {
Parameters::gradual_form()
};
Type::GenericAlias(_) => {
// TODO annotated return type on `__new__` or metaclass `__call__`
// TODO check call vs signatures of `__new__` and/or `__init__`
Binding::single(self, Signature::new(parameters, Some(instantiated))).into()
Binding::single(
self,
Signature::new(Parameters::gradual_form(), self.to_instance(db)),
)
.into()
}
Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
@ -4636,7 +4619,7 @@ impl<'db> Type<'db> {
}
if let Type::GenericAlias(alias) = self {
if alias.origin(db).is_known(db, KnownClass::Tuple) {
if alias.origin(db).is_tuple(db) {
return Ok(todo_type!("*tuple[] annotations"));
}
}

View file

@ -575,7 +575,7 @@ impl<'db> ClassType<'db> {
pub(super) fn own_class_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
let (class_literal, specialization) = self.class_literal(db);
let synthesize_tuple_method = |return_type| {
let synthesize_simple_tuple_method = |return_type| {
let parameters =
Parameters::new([Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(Type::instance(db, self))]);
@ -587,22 +587,88 @@ impl<'db> ClassType<'db> {
};
match name {
"__len__" if class_literal.is_known(db, KnownClass::Tuple) => {
"__len__" if class_literal.is_tuple(db) => {
let return_type = specialization
.and_then(|spec| spec.tuple(db).len().into_fixed_length())
.and_then(|len| i64::try_from(len).ok())
.map(Type::IntLiteral)
.unwrap_or_else(|| KnownClass::Int.to_instance(db));
synthesize_tuple_method(return_type)
synthesize_simple_tuple_method(return_type)
}
"__bool__" if class_literal.is_known(db, KnownClass::Tuple) => {
"__bool__" if class_literal.is_tuple(db) => {
let return_type = specialization
.map(|spec| spec.tuple(db).truthiness().into_type(db))
.unwrap_or_else(|| KnownClass::Bool.to_instance(db));
synthesize_tuple_method(return_type)
synthesize_simple_tuple_method(return_type)
}
// ```py
// class tuple:
// @overload
// def __new__(cls: type[tuple[()]], iterable: tuple[()] = ()) -> tuple[()]: ...
// @overload
// def __new__[T](cls: type[tuple[T, ...]], iterable: tuple[T, ...]) -> tuple[T, ...]: ...
// ```
"__new__" if class_literal.is_tuple(db) => {
let mut iterable_parameter =
Parameter::positional_only(Some(Name::new_static("iterable")));
match specialization {
Some(spec) => {
let tuple = spec.tuple(db);
let tuple_len = tuple.len();
if tuple_len.minimum() == 0 && tuple_len.maximum().is_none() {
// If the tuple has no length restrictions,
// any iterable is allowed as long as the iterable has the correct element type.
let mut tuple_elements = tuple.all_elements();
iterable_parameter = iterable_parameter.with_annotated_type(
KnownClass::Iterable
.to_specialized_instance(db, [*tuple_elements.next().unwrap()]),
);
assert_eq!(
tuple_elements.next(),
None,
"Tuple specialization should not have more than one element when it has no length restriction"
);
} else {
// But if the tuple is of a fixed length, or has a minimum length, we require a tuple rather
// than an iterable, as a tuple is the only kind of iterable for which we can
// specify a fixed length, or that the iterable must be at least a certain length.
iterable_parameter =
iterable_parameter.with_annotated_type(Type::instance(db, self));
}
}
None => {
// If the tuple isn't specialized at all, we allow any argument as long as it is iterable.
iterable_parameter = iterable_parameter
.with_annotated_type(KnownClass::Iterable.to_instance(db));
}
}
// We allow the `iterable` parameter to be omitted for:
// - a zero-length tuple
// - an unspecialized tuple
// - a tuple with no minimum length
if specialization.is_none_or(|spec| spec.tuple(db).len().minimum() == 0) {
iterable_parameter = iterable_parameter.with_default_type(TupleType::empty(db));
}
let parameters = Parameters::new([
Parameter::positional_only(Some(Name::new_static("self")))
.with_annotated_type(SubclassOfType::from(db, self)),
iterable_parameter,
]);
let synthesized_dunder =
CallableType::function_like(db, Signature::new(parameters, None));
Place::bound(synthesized_dunder).into()
}
_ => class_literal
.own_class_member(db, specialization, name)
.map_type(|ty| ty.apply_optional_specialization(db, specialization)),
@ -659,38 +725,41 @@ impl<'db> ClassType<'db> {
)
.place;
let dunder_new_function =
if let Place::Type(Type::FunctionLiteral(dunder_new_function), _) =
dunder_new_function_symbol
{
// Step 3: If the return type of the `__new__` evaluates to a type that is not a subclass of this class,
// then we should ignore the `__init__` and just return the `__new__` method.
let returns_non_subclass =
dunder_new_function
.signature(db)
.overloads
.iter()
.any(|signature| {
signature.return_ty.is_some_and(|return_ty| {
!return_ty.is_assignable_to(
db,
self_ty
.to_instance(db)
.expect("ClassType should be instantiable"),
)
})
});
let dunder_new_signature = dunder_new_function_symbol
.ignore_possibly_unbound()
.and_then(|ty| match ty {
Type::FunctionLiteral(function) => Some(function.signature(db)),
Type::Callable(callable) => Some(callable.signatures(db)),
_ => None,
});
let dunder_new_bound_method =
dunder_new_function.into_bound_method_type(db, self_ty);
let dunder_new_function = if let Some(dunder_new_signature) = dunder_new_signature {
// Step 3: If the return type of the `__new__` evaluates to a type that is not a subclass of this class,
// then we should ignore the `__init__` and just return the `__new__` method.
let returns_non_subclass = dunder_new_signature.overloads.iter().any(|signature| {
signature.return_ty.is_some_and(|return_ty| {
!return_ty.is_assignable_to(
db,
self_ty
.to_instance(db)
.expect("ClassType should be instantiable"),
)
})
});
if returns_non_subclass {
return dunder_new_bound_method;
}
Some(dunder_new_bound_method)
} else {
None
};
let dunder_new_bound_method = Type::Callable(CallableType::new(
db,
dunder_new_signature.bind_self(),
true,
));
if returns_non_subclass {
return dunder_new_bound_method;
}
Some(dunder_new_bound_method)
} else {
None
};
let dunder_init_function_symbol = self_ty
.member_lookup_with_policy(
@ -864,6 +933,10 @@ impl<'db> ClassLiteral<'db> {
self.known(db) == Some(known_class)
}
pub(crate) fn is_tuple(self, db: &'db dyn Db) -> bool {
self.is_known(db, KnownClass::Tuple)
}
pub(crate) fn generic_context(self, db: &'db dyn Db) -> Option<GenericContext<'db>> {
// Several typeshed definitions examine `sys.version_info`. To break cycles, we hard-code
// the knowledge that this class is not generic.

View file

@ -511,7 +511,7 @@ impl TupleSpecialization {
}
fn from_class(db: &dyn Db, class: ClassLiteral) -> Self {
if class.is_known(db, KnownClass::Tuple) {
if class.is_tuple(db) {
Self::Yes
} else {
Self::No

View file

@ -5637,10 +5637,16 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
| KnownClass::TypeVar
| KnownClass::NamedTuple
| KnownClass::TypeAliasType
| KnownClass::Tuple
| KnownClass::Deprecated
)
)
// Constructor calls to `tuple` and subclasses of `tuple` are handled in `Type::Bindings`,
// but constructor calls to `tuple[int]`, `tuple[int, ...]`, `tuple[int, *tuple[str, ...]]` (etc.)
// are handled by the default constructor-call logic (we synthesize a `__new__` method for them
// in `ClassType::own_class_member()`).
&& (callable_type.is_generic_alias() || !class.is_known(self.db(), KnownClass::Tuple))
// temporary special-casing for all subclasses of `enum.Enum`
// until we support the functional syntax for creating enum classes
&& KnownClass::Enum
@ -8003,7 +8009,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// updating all of the subscript logic below to use custom callables for all of the _other_
// special cases, too.
if let Type::ClassLiteral(class) = value_ty {
if class.is_known(self.db(), KnownClass::Tuple) {
if class.is_tuple(self.db()) {
return self
.infer_tuple_type_expression(slice)
.to_meta_type(self.db());