[ty] Understand homogeneous tuple annotations (#17998)

This commit is contained in:
Alex Waygood 2025-05-12 22:02:25 -04:00 committed by GitHub
parent f301931159
commit 55df9271ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 196 additions and 104 deletions

View file

@ -1273,17 +1273,8 @@ impl<'db> Type<'db> {
)
}
// Other than the special tuple-to-tuple case handled, above,
// tuple subtyping delegates to `Instance(tuple)` in the same way as the literal types.
//
// All heterogeneous tuple types are subtypes of `Instance(<tuple>)`:
// `Instance(<some class T>)` expresses "the set of all possible instances of the class `T`";
// consequently, `Instance(<tuple>)` expresses "the set of all possible instances of the class `tuple`".
// This type can be spelled in type annotations as `tuple[object, ...]` (since `tuple` is covariant).
//
// Note that this is not the same type as the type spelled in type annotations as `tuple`;
// as that type is equivalent to `type[Any, ...]` (and therefore not a fully static type).
(Type::Tuple(_), _) => KnownClass::Tuple.to_instance(db).is_subtype_of(db, target),
// `tuple[A, B, C]` is a subtype of `tuple[A | B | C, ...]`
(Type::Tuple(tuple), _) => tuple.homogeneous_supertype(db).is_subtype_of(db, target),
(Type::BoundSuper(_), Type::BoundSuper(_)) => self.is_equivalent_to(db, target),
(Type::BoundSuper(_), _) => KnownClass::Super.to_instance(db).is_subtype_of(db, target),
@ -1494,10 +1485,10 @@ impl<'db> Type<'db> {
// This special case is required because the left-hand side tuple might be a
// gradual type, so we can not rely on subtyping. This allows us to assign e.g.
// `tuple[Any, int]` to `tuple`.
(Type::Tuple(_), _)
if KnownClass::Tuple
.to_instance(db)
.is_assignable_to(db, target) =>
//
// `tuple[A, B, C]` is assignable to `tuple[A | B | C, ...]`
(Type::Tuple(tuple), _)
if tuple.homogeneous_supertype(db).is_assignable_to(db, target) =>
{
true
}
@ -1960,9 +1951,9 @@ impl<'db> Type<'db> {
!known_instance.is_instance_of(db, instance.class())
}
(known_instance_ty @ Type::KnownInstance(_), Type::Tuple(_))
| (Type::Tuple(_), known_instance_ty @ Type::KnownInstance(_)) => {
known_instance_ty.is_disjoint_from(db, KnownClass::Tuple.to_instance(db))
(known_instance_ty @ Type::KnownInstance(_), Type::Tuple(tuple))
| (Type::Tuple(tuple), known_instance_ty @ Type::KnownInstance(_)) => {
known_instance_ty.is_disjoint_from(db, tuple.homogeneous_supertype(db))
}
(Type::BooleanLiteral(..), Type::NominalInstance(instance))
@ -2088,17 +2079,9 @@ impl<'db> Type<'db> {
.any(|(e1, e2)| e1.is_disjoint_from(db, *e2))
}
(Type::Tuple(..), instance @ Type::NominalInstance(_))
| (instance @ Type::NominalInstance(_), Type::Tuple(..)) => {
// We cannot be sure if the tuple is disjoint from the instance because:
// - 'other' might be the homogeneous arbitrary-length tuple type
// tuple[T, ...] (which we don't have support for yet); if all of
// our element types are not disjoint with T, this is not disjoint
// - 'other' might be a user subtype of tuple, which, if generic
// over the same or compatible *Ts, would overlap with tuple.
//
// TODO: add checks for the above cases once we support them
instance.is_disjoint_from(db, KnownClass::Tuple.to_instance(db))
(Type::Tuple(tuple), instance @ Type::NominalInstance(_))
| (instance @ Type::NominalInstance(_), Type::Tuple(tuple)) => {
instance.is_disjoint_from(db, tuple.homogeneous_supertype(db))
}
(Type::PropertyInstance(_), other) | (other, Type::PropertyInstance(_)) => {
@ -2588,7 +2571,7 @@ impl<'db> Type<'db> {
KnownClass::Str.to_instance(db).instance_member(db, name)
}
Type::BytesLiteral(_) => KnownClass::Bytes.to_instance(db).instance_member(db, name),
Type::Tuple(_) => KnownClass::Tuple.to_instance(db).instance_member(db, name),
Type::Tuple(tuple) => tuple.homogeneous_supertype(db).instance_member(db, name),
Type::AlwaysTruthy | Type::AlwaysFalsy => Type::object(db).instance_member(db, name),
Type::ModuleLiteral(_) => KnownClass::ModuleType
@ -3573,8 +3556,10 @@ impl<'db> Type<'db> {
db,
[
KnownClass::Str.to_instance(db),
// TODO: tuple[str, ...]
KnownClass::Tuple.to_instance(db),
KnownClass::Tuple.to_specialized_instance(
db,
[KnownClass::Str.to_instance(db)],
),
],
)),
Parameter::positional_only(Some(Name::new_static("start")))
@ -3854,6 +3839,9 @@ impl<'db> Type<'db> {
}
Some(KnownClass::Type) => {
let str_instance = KnownClass::Str.to_instance(db);
let type_instance = KnownClass::Type.to_instance(db);
// ```py
// class type:
// @overload
@ -3869,20 +3857,26 @@ impl<'db> Type<'db> {
Name::new_static("o"),
))
.with_annotated_type(Type::any())]),
Some(KnownClass::Type.to_instance(db)),
Some(type_instance),
),
Signature::new(
Parameters::new([
Parameter::positional_only(Some(Name::new_static("name")))
.with_annotated_type(KnownClass::Str.to_instance(db)),
.with_annotated_type(str_instance),
Parameter::positional_only(Some(Name::new_static("bases")))
// TODO: Should be tuple[type, ...] once we have support for homogenous tuples
.with_annotated_type(KnownClass::Tuple.to_instance(db)),
.with_annotated_type(
KnownClass::Tuple
.to_specialized_instance(db, [type_instance]),
),
Parameter::positional_only(Some(Name::new_static("dict")))
// TODO: Should be `dict[str, Any]` once we have support for generics
.with_annotated_type(KnownClass::Dict.to_instance(db)),
.with_annotated_type(
KnownClass::Dict.to_specialized_instance(
db,
[str_instance, Type::any()],
),
),
]),
Some(KnownClass::Type.to_instance(db)),
Some(type_instance),
),
],
);
@ -7924,6 +7918,11 @@ pub struct TupleType<'db> {
}
impl<'db> TupleType<'db> {
fn homogeneous_supertype(self, db: &'db dyn Db) -> Type<'db> {
KnownClass::Tuple
.to_specialized_instance(db, [UnionType::from_elements(db, self.elements(db))])
}
pub(crate) fn from_elements<T: Into<Type<'db>>>(
db: &'db dyn Db,
types: impl IntoIterator<Item = T>,

View file

@ -2107,9 +2107,13 @@ impl<'db> TypeInferenceBuilder<'db> {
definition: Definition<'db>,
) {
if let Some(annotation) = parameter.annotation() {
let _annotated_ty = self.file_expression_type(annotation);
// TODO `tuple[annotated_type, ...]`
let ty = KnownClass::Tuple.to_instance(self.db());
let ty = if annotation.is_starred_expr() {
todo_type!("PEP 646")
} else {
let annotated_type = self.file_expression_type(annotation);
KnownClass::Tuple.to_specialized_instance(self.db(), [annotated_type])
};
self.add_declaration_with_binding(
parameter.into(),
definition,
@ -2119,8 +2123,7 @@ impl<'db> TypeInferenceBuilder<'db> {
self.add_binding(
parameter.into(),
definition,
// TODO `tuple[Unknown, ...]`
KnownClass::Tuple.to_instance(self.db()),
KnownClass::Tuple.to_specialized_instance(self.db(), [Type::unknown()]),
);
}
}
@ -2473,16 +2476,32 @@ impl<'db> TypeInferenceBuilder<'db> {
except_handler_definition: &ExceptHandlerDefinitionKind,
definition: Definition<'db>,
) {
fn extract_tuple_specialization<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Type<'db>> {
let class = ty.into_nominal_instance()?.class();
if !class.is_known(db, KnownClass::Tuple) {
return None;
}
let ClassType::Generic(class) = class else {
return None;
};
let specialization = class.specialization(db).types(db)[0];
let specialization_instance = specialization.to_instance(db)?;
specialization_instance
.is_assignable_to(db, KnownClass::BaseException.to_instance(db))
.then_some(specialization_instance)
}
let node = except_handler_definition.handled_exceptions();
// If there is no handled exception, it's invalid syntax;
// a diagnostic will have already been emitted
let node_ty = node.map_or(Type::unknown(), |ty| self.infer_expression(ty));
let type_base_exception = KnownClass::BaseException.to_subclass_of(self.db());
// If it's an `except*` handler, this won't actually be the type of the bound symbol;
// it will actually be the type of the generic parameters to `BaseExceptionGroup` or `ExceptionGroup`.
let symbol_ty = if let Type::Tuple(tuple) = node_ty {
let type_base_exception = KnownClass::BaseException.to_subclass_of(self.db());
let mut builder = UnionBuilder::new(self.db());
for element in tuple.elements(self.db()).iter().copied() {
builder = builder.add(
@ -2500,27 +2519,37 @@ impl<'db> TypeInferenceBuilder<'db> {
);
}
builder.build()
} else if node_ty.is_subtype_of(self.db(), KnownClass::Tuple.to_instance(self.db())) {
todo_type!("Homogeneous tuple in exception handler")
} else if node_ty.is_assignable_to(self.db(), type_base_exception) {
node_ty.to_instance(self.db()).expect(
"`Type::to_instance()` should always return `Some()` \
if called on a type assignable to `type[BaseException]`",
)
} else if node_ty.is_assignable_to(
self.db(),
KnownClass::Tuple.to_specialized_instance(self.db(), [type_base_exception]),
) {
extract_tuple_specialization(self.db(), node_ty)
.unwrap_or_else(|| KnownClass::BaseException.to_instance(self.db()))
} else if node_ty.is_assignable_to(
self.db(),
UnionType::from_elements(
self.db(),
[
type_base_exception,
KnownClass::Tuple.to_specialized_instance(self.db(), [type_base_exception]),
],
),
) {
KnownClass::BaseException.to_instance(self.db())
} else {
let type_base_exception = KnownClass::BaseException.to_subclass_of(self.db());
if node_ty.is_assignable_to(self.db(), type_base_exception) {
node_ty.to_instance(self.db()).expect(
"`Type::to_instance()` should always return `Some()` \
if called on a type assignable to `type[BaseException]`",
)
} else {
if let Some(node) = node {
report_invalid_exception_caught(&self.context, node, node_ty);
}
Type::unknown()
if let Some(node) = node {
report_invalid_exception_caught(&self.context, node, node_ty);
}
Type::unknown()
};
let symbol_ty = if except_handler_definition.is_star() {
// TODO: we should infer `ExceptionGroup` if `node_ty` is a subtype of `tuple[type[Exception], ...]`
// (needs support for homogeneous tuples).
//
// TODO: should be generic with `symbol_ty` as the generic parameter
KnownClass::BaseExceptionGroup.to_instance(self.db())
} else {
@ -7970,7 +7999,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
match element {
ast::Expr::EllipsisLiteral(_) | ast::Expr::Starred(_) => true,
ast::Expr::Starred(_) => true,
ast::Expr::Subscript(ast::ExprSubscript { value, .. }) => {
let value_ty = if builder.deferred_state.in_string_annotation() {
// Using `.expression_type` does not work in string annotations, because
@ -7980,17 +8009,23 @@ impl<'db> TypeInferenceBuilder<'db> {
builder.expression_type(value)
};
matches!(value_ty, Type::KnownInstance(KnownInstanceType::Unpack))
value_ty == Type::KnownInstance(KnownInstanceType::Unpack)
}
_ => false,
}
}
// TODO:
// - homogeneous tuples
// - PEP 646
// TODO: PEP 646
match tuple_slice {
ast::Expr::Tuple(elements) => {
if let [element, ellipsis @ ast::Expr::EllipsisLiteral(_)] = &*elements.elts {
self.infer_expression(ellipsis);
let result = KnownClass::Tuple
.to_specialized_instance(self.db(), [self.infer_type_expression(element)]);
self.store_expression_type(tuple_slice, result);
return result;
}
let mut element_types = Vec::with_capacity(elements.len());
// Whether to infer `Todo` for the whole tuple
@ -8005,7 +8040,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
let ty = if return_todo {
todo_type!("full tuple[...] support")
todo_type!("PEP 646")
} else {
TupleType::from_elements(self.db(), element_types)
};
@ -8021,7 +8056,7 @@ impl<'db> TypeInferenceBuilder<'db> {
let single_element_ty = self.infer_type_expression(single_element);
if element_could_alter_type_of_whole_tuple(single_element, single_element_ty, self)
{
todo_type!("full tuple[...] support")
todo_type!("PEP 646")
} else {
TupleType::from_elements(self.db(), std::iter::once(single_element_ty))
}