From f3d12c4aac757cdb653e57d2b45c93d1f9bb6bcb Mon Sep 17 00:00:00 2001 From: David Peter Date: Fri, 21 Nov 2025 14:13:21 +0100 Subject: [PATCH] [ty] Allow specialization of generic classes with TypeVar instances --- .../resources/mdtest/implicit_type_aliases.md | 11 +++--- crates/ty_python_semantic/src/types.rs | 4 ++ .../src/types/infer/builder.rs | 37 +++++++++++++++---- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md index 4b28f150bc..6f2748446d 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -389,9 +389,8 @@ ListOrTupleLegacy = Union[list[T], tuple[T, ...]] MyCallable = Callable[P, T] AnnotatedType = Annotated[T, "tag"] -# TODO: Consider displaying this as ``, … instead? (and similar for some others below) -reveal_type(MyList) # revealed: -reveal_type(MyDict) # revealed: +reveal_type(MyList) # revealed: +reveal_type(MyDict) # revealed: reveal_type(MyType) # revealed: GenericAlias reveal_type(IntAndType) # revealed: reveal_type(Pair) # revealed: @@ -496,9 +495,9 @@ def _( my_callable: MyCallable, ): # TODO: Should be `list[Unknown]` - reveal_type(my_list) # revealed: list[typing.TypeVar] + reveal_type(my_list) # revealed: list[T] # TODO: Should be `dict[Unknown, Unknown]` - reveal_type(my_dict) # revealed: dict[typing.TypeVar, typing.TypeVar] + reveal_type(my_dict) # revealed: dict[T, U] # TODO: Should be `(...) -> Unknown` reveal_type(my_callable) # revealed: (...) -> typing.TypeVar ``` @@ -523,7 +522,7 @@ reveal_mro(Derived1) GenericBaseAlias = GenericBase[T] # TODO: No error here -# error: [non-subscriptable] "Cannot subscript object of type `` with no `__class_getitem__` method" +# error: [non-subscriptable] "Cannot subscript object of type `` with no `__class_getitem__` method" class Derived2(GenericBaseAlias[int]): pass ``` diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 5447ec84a6..da20ded891 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -7197,6 +7197,8 @@ impl<'db> Type<'db> { instance.apply_type_mapping_impl(db, type_mapping, tcx, visitor) }, + Type::NewTypeInstance(_) if matches!(type_mapping, TypeMapping::BindLegacyTypevars(_)) => self, + Type::NewTypeInstance(newtype) => visitor.visit(self, || { Type::NewTypeInstance(newtype.map_base_class_type(db, |class_type| { class_type.apply_type_mapping_impl(db, type_mapping, tcx, visitor) @@ -7275,6 +7277,8 @@ impl<'db> Type<'db> { // TODO(jelle): Materialize should be handled differently, since TypeIs is invariant Type::TypeIs(type_is) => type_is.with_type(db, type_is.return_type(db).apply_type_mapping(db, type_mapping, tcx)), + Type::TypeAlias(_) if matches!(type_mapping, TypeMapping::BindLegacyTypevars(_)) => self, + Type::TypeAlias(alias) => { // Do not call `value_type` here. `value_type` does the specialization internally, so `apply_type_mapping` is performed without `visitor` inheritance. // In the case of recursive type aliases, this leads to infinite recursion. diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index cd93f59526..38b3f969cc 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -101,14 +101,14 @@ use crate::types::typed_dict::{ }; use crate::types::visitor::any_over_type; use crate::types::{ - CallDunderError, CallableBinding, CallableType, ClassLiteral, ClassType, DataclassParams, - DynamicType, InternedType, IntersectionBuilder, IntersectionType, KnownClass, + BindingContext, CallDunderError, CallableBinding, CallableType, ClassLiteral, ClassType, + DataclassParams, DynamicType, InternedType, IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter, ParameterForm, Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext, - TypeQualifiers, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity, - TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType, - UnionTypeInstance, binding_type, todo_type, + TypeMapping, TypeQualifiers, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, + TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, + UnionType, UnionTypeInstance, binding_type, todo_type, }; use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic}; use crate::unpack::{EvaluationMode, UnpackPosition}; @@ -11046,6 +11046,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { value_ty, generic_context, specialize, + true, ) } @@ -11070,6 +11071,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { value_ty, generic_context, specialize, + false, ) } @@ -11079,12 +11081,30 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { value_ty: Type<'db>, generic_context: GenericContext<'db>, specialize: impl FnOnce(&[Option>]) -> Type<'db>, + bind_legacy_typevars: bool, ) -> Type<'db> { let slice_node = subscript.slice.as_ref(); + + let db = self.db(); + let do_bind_legacy_typevars = |ty: Type<'db>| { + if bind_legacy_typevars { + ty.apply_type_mapping( + db, + &TypeMapping::BindLegacyTypevars(BindingContext::Synthetic), + TypeContext::default(), + ) + } else { + ty + } + }; + let call_argument_types = match slice_node { ast::Expr::Tuple(tuple) => { let arguments = CallArguments::positional( - tuple.elts.iter().map(|elt| self.infer_type_expression(elt)), + tuple + .elts + .iter() + .map(|elt| do_bind_legacy_typevars(self.infer_type_expression(elt))), ); self.store_expression_type( slice_node, @@ -11092,8 +11112,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ); arguments } - _ => CallArguments::positional([self.infer_type_expression(slice_node)]), + _ => CallArguments::positional([do_bind_legacy_typevars( + self.infer_type_expression(slice_node), + )]), }; + let binding = Binding::single(value_ty, generic_context.signature(self.db())); let bindings = match Bindings::from(binding) .match_parameters(self.db(), &call_argument_types)