From 41207ec901340423d988b85dc8138e306f9a39c2 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 4 Aug 2025 14:10:47 +0100 Subject: [PATCH] [ty] Infer `type[tuple[int, str]]` as the meta-type of `tuple[int, str]` (#19741) --- .../resources/mdtest/attributes.md | 2 +- .../resources/mdtest/type_compendium/tuple.md | 22 +++++++++++ crates/ty_python_semantic/src/types.rs | 13 +++++-- crates/ty_python_semantic/src/types/class.rs | 4 ++ .../src/types/ide_support.rs | 5 +++ crates/ty_python_semantic/src/types/infer.rs | 39 ++++++++++++------- crates/ty_python_semantic/src/types/tuple.rs | 7 +++- 7 files changed, 71 insertions(+), 21 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/attributes.md b/crates/ty_python_semantic/resources/mdtest/attributes.md index 1d31b08118..2afe8617ac 100644 --- a/crates/ty_python_semantic/resources/mdtest/attributes.md +++ b/crates/ty_python_semantic/resources/mdtest/attributes.md @@ -1916,7 +1916,7 @@ d = True reveal_type(d.__class__) # revealed: e = (42, 42) -reveal_type(e.__class__) # revealed: +reveal_type(e.__class__) # revealed: type[tuple[Literal[42], Literal[42]]] def f(a: int, b: typing_extensions.LiteralString, c: int | str, d: type[str]): reveal_type(a.__class__) # revealed: type[int] diff --git a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md index fee5e30fd2..2419eead01 100644 --- a/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md +++ b/crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md @@ -119,6 +119,28 @@ def _(empty: EmptyTupleSubclass, single_element: SingleElementTupleSubclass, mix mixed.__class__() ``` +## Meta-type of tuple instances + +The type `tuple[str, int]` does not only have exact instances of `tuple` as its inhabitants: its +inhabitants also include any instances of subclasses of `tuple[str, int]`. As such, the meta-type of +`tuple[str, int]` should be `type[tuple[str, int]]` rather than ``. The +former accurately reflects the fact that given an instance of `tuple[str, int]`, we do not know +exactly what the `__class__` of that instance will be: we only know that it will be a subclass of +`tuple[str, int]`. The latter would be incorrectly precise: it would imply that all instances of +`tuple[str, int]` have the runtime object `tuple` as their `__class__`, which isn't true. + +```toml +[environment] +python-version = "3.11" +``` + +```py +def f(x: tuple[int, ...], y: tuple[str, str], z: tuple[int, *tuple[str, ...], bytes]): + reveal_type(type(x)) # revealed: type[tuple[int, ...]] + reveal_type(type(y)) # revealed: type[tuple[str, str]] + reveal_type(type(z)) # revealed: type[tuple[int, *tuple[str, ...], bytes]] +``` + ## Subtyping relationships The type `tuple[S1, S2]` is a subtype of `tuple[T1, T2]` if and only if `S1` is a subtype of `T1` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index e7f19f03fd..bf2bc151fc 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -790,6 +790,13 @@ impl<'db> Type<'db> { } } + pub const fn into_subclass_of(self) -> Option> { + match self { + Type::SubclassOf(subclass_of) => Some(subclass_of), + _ => None, + } + } + #[track_caller] pub fn expect_class_literal(self) -> ClassLiteral<'db> { self.into_class_literal() @@ -5496,10 +5503,8 @@ impl<'db> Type<'db> { Type::Callable(_) | Type::DataclassTransformer(_) => KnownClass::Type.to_instance(db), Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class_literal(db), Type::Tuple(tuple) => tuple - .to_class_type(db) - .map(Type::from) - .unwrap_or_else(Type::unknown), - + .to_subclass_of(db) + .unwrap_or_else(SubclassOfType::subclass_of_unknown), Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { None => KnownClass::Type.to_instance(db), Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.to_meta_type(db), diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 2e62307087..ddb84424d5 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -268,6 +268,10 @@ pub enum ClassType<'db> { #[salsa::tracked] impl<'db> ClassType<'db> { + pub(super) const fn is_not_generic(self) -> bool { + matches!(self, Self::NonGeneric(_)) + } + pub(super) fn normalized_impl( self, db: &'db dyn Db, diff --git a/crates/ty_python_semantic/src/types/ide_support.rs b/crates/ty_python_semantic/src/types/ide_support.rs index efbe151702..d04fda4150 100644 --- a/crates/ty_python_semantic/src/types/ide_support.rs +++ b/crates/ty_python_semantic/src/types/ide_support.rs @@ -143,6 +143,11 @@ impl<'db> AllMembers<'db> { Type::ClassLiteral(class_literal) => { self.extend_with_class_members(db, ty, class_literal); } + Type::SubclassOf(subclass_of) => { + if let Some(class) = subclass_of.subclass_of().into_class() { + self.extend_with_class_members(db, ty, class.class_literal(db).0); + } + } Type::GenericAlias(generic_alias) => { let class_literal = generic_alias.origin(db); self.extend_with_class_members(db, ty, class_literal); diff --git a/crates/ty_python_semantic/src/types/infer.rs b/crates/ty_python_semantic/src/types/infer.rs index bd88c61f21..7043e31ba5 100644 --- a/crates/ty_python_semantic/src/types/infer.rs +++ b/crates/ty_python_semantic/src/types/infer.rs @@ -6069,7 +6069,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // the `try_call` path below. // TODO: it should be possible to move these special cases into the `try_call_constructor` // path instead, or even remove some entirely once we support overloads fully. - if !matches!( + let has_special_cased_constructor = matches!( class.known(self.db()), Some( KnownClass::Bool @@ -6083,20 +6083,21 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { | KnownClass::TypeAliasType | 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)) + ) || ( + // 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()`). + class.is_known(self.db(), KnownClass::Tuple) && class.is_not_generic() + ); // temporary special-casing for all subclasses of `enum.Enum` // until we support the functional syntax for creating enum classes - && KnownClass::Enum - .to_class_literal(self.db()) - .to_class_type(self.db()) - .is_none_or(|enum_class| !class.is_subclass_of(self.db(), enum_class)) + if !has_special_cased_constructor + && KnownClass::Enum + .to_class_literal(self.db()) + .to_class_type(self.db()) + .is_none_or(|enum_class| !class.is_subclass_of(self.db(), enum_class)) { let argument_forms = vec![Some(ParameterForm::Value); call_arguments.len()]; self.infer_argument_types(arguments, &mut call_arguments, &argument_forms); @@ -8478,6 +8479,15 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } + let tuple_generic_alias = |db: &'db dyn Db, tuple: Option>| { + let tuple = + tuple.unwrap_or_else(|| TupleType::homogeneous(db, Type::unknown()).unwrap()); + tuple + .to_class_type(db) + .map(Type::from) + .unwrap_or_else(Type::unknown) + }; + // HACK ALERT: If we are subscripting a generic class, short-circuit the rest of the // subscript inference logic and treat this as an explicit specialization. // TODO: Move this logic into a custom callable, and update `find_name_in_mro` to return @@ -8486,8 +8496,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // special cases, too. if let Type::ClassLiteral(class) = value_ty { if class.is_tuple(self.db()) { - return Type::tuple(self.infer_tuple_type_expression(slice)) - .to_meta_type(self.db()); + return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(slice)); } if let Some(generic_context) = class.generic_context(self.db()) { return self.infer_explicit_class_specialization( @@ -8499,7 +8508,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } if let Type::SpecialForm(SpecialFormType::Tuple) = value_ty { - return Type::tuple(self.infer_tuple_type_expression(slice)).to_meta_type(self.db()); + return tuple_generic_alias(self.db(), self.infer_tuple_type_expression(slice)); } let slice_ty = self.infer_expression(slice); diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index 63c43aa0b9..8200e4658f 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -22,8 +22,8 @@ use std::hash::Hash; use itertools::{Either, EitherOrBoth, Itertools}; -use crate::types::Truthiness; use crate::types::class::{ClassType, KnownClass}; +use crate::types::{SubclassOfType, Truthiness}; use crate::types::{ Type, TypeMapping, TypeRelation, TypeTransformer, TypeVarInstance, TypeVarVariance, UnionBuilder, UnionType, cyclic::PairVisitor, @@ -238,6 +238,11 @@ impl<'db> TupleType<'db> { }) } + pub(crate) fn to_subclass_of(self, db: &'db dyn Db) -> Option> { + self.to_class_type(db) + .map(|class| SubclassOfType::from(db, class)) + } + /// Return a normalized version of `self`. /// /// See [`Type::normalized`] for more details.