From e6ddeed38667db198424fa276e3efdbd8cea3caa Mon Sep 17 00:00:00 2001 From: David Peter Date: Wed, 3 Dec 2025 09:10:45 +0100 Subject: [PATCH] [ty] Default-specialization of generic type aliases (#21765) ## Summary Implement default-specialization of generic type aliases (implicit or PEP-613) if they are used in a type expression without an explicit specialization. closes https://github.com/astral-sh/ty/issues/1690 ## Typing conformance ```diff -generics_defaults_specialization.py:26:5: error[type-assertion-failure] Type `SomethingWithNoDefaults[int, str]` does not match asserted type `SomethingWithNoDefaults[int, DefaultStrT]` ``` That's exactly what we want :heavy_check_mark: All other tests in this file pass as well, with the exception of this assertion, which is just wrong (at least according to our interpretation, `type[Bar] != `). I checked that we do correctly default-specialize the type parameter which is not displayed in the diagnostic that we raise. ```py class Bar(SubclassMe[int, DefaultStrT]): ... assert_type(Bar, type[Bar[str]]) # ty: Type `type[Bar[str]]` does not match asserted type `` ``` ## Ecosystem impact Looks like I should have included this last week :sunglasses: ## Test Plan Updated pre-existing tests and add a few new ones. --- .../resources/mdtest/implicit_type_aliases.md | 57 ++++++++----------- crates/ty_python_semantic/src/types.rs | 10 ++++ .../infer/builder/annotation_expression.rs | 23 ++++---- .../types/infer/builder/type_expression.rs | 2 + 4 files changed, 49 insertions(+), 43 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 8207d55bcc..ed73c9323b 100644 --- a/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md +++ b/crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md @@ -190,14 +190,10 @@ def _( reveal_type(type_of_str_or_int) # revealed: type[str] | int reveal_type(int_or_callable) # revealed: int | ((str, /) -> bytes) reveal_type(callable_or_int) # revealed: ((str, /) -> bytes) | int - # TODO should be Unknown | int - reveal_type(type_var_or_int) # revealed: T@TypeVarOrInt | int - # TODO should be int | Unknown - reveal_type(int_or_type_var) # revealed: int | T@IntOrTypeVar - # TODO should be Unknown | None - reveal_type(type_var_or_none) # revealed: T@TypeVarOrNone | None - # TODO should be None | Unknown - reveal_type(none_or_type_var) # revealed: None | T@NoneOrTypeVar + reveal_type(type_var_or_int) # revealed: Unknown | int + reveal_type(int_or_type_var) # revealed: int | Unknown + reveal_type(type_var_or_none) # revealed: Unknown | None + reveal_type(none_or_type_var) # revealed: None | Unknown ``` If a type is unioned with itself in a value expression, the result is just that type. No @@ -529,28 +525,18 @@ def _( annotated_unknown: AnnotatedType, optional_unknown: MyOptional, ): - # TODO: This should be `list[Unknown]` - reveal_type(list_unknown) # revealed: list[T@MyList] - # TODO: This should be `dict[Unknown, Unknown]` - reveal_type(dict_unknown) # revealed: dict[T@MyDict, U@MyDict] - # TODO: Should be `type[Unknown]` - reveal_type(subclass_of_unknown) # revealed: type[T@MyType] - # TODO: Should be `tuple[int, Unknown]` - reveal_type(int_and_unknown) # revealed: tuple[int, T@IntAndType] - # TODO: Should be `tuple[Unknown, Unknown]` - reveal_type(pair_of_unknown) # revealed: tuple[T@Pair, T@Pair] - # TODO: Should be `tuple[Unknown, Unknown]` - reveal_type(unknown_and_unknown) # revealed: tuple[T@Sum, U@Sum] - # TODO: Should be `list[Unknown] | tuple[Unknown, ...]` - reveal_type(list_or_tuple) # revealed: list[T@ListOrTuple] | tuple[T@ListOrTuple, ...] - # TODO: Should be `list[Unknown] | tuple[Unknown, ...]` - reveal_type(list_or_tuple_legacy) # revealed: list[T@ListOrTupleLegacy] | tuple[T@ListOrTupleLegacy, ...] - # TODO: Should be `(...) -> Unknown` + reveal_type(list_unknown) # revealed: list[Unknown] + reveal_type(dict_unknown) # revealed: dict[Unknown, Unknown] + reveal_type(subclass_of_unknown) # revealed: type[Unknown] + reveal_type(int_and_unknown) # revealed: tuple[int, Unknown] + reveal_type(pair_of_unknown) # revealed: tuple[Unknown, Unknown] + reveal_type(unknown_and_unknown) # revealed: tuple[Unknown, Unknown] + reveal_type(list_or_tuple) # revealed: list[Unknown] | tuple[Unknown, ...] + reveal_type(list_or_tuple_legacy) # revealed: list[Unknown] | tuple[Unknown, ...] + # TODO: should be (...) -> Unknown reveal_type(my_callable) # revealed: @Todo(Callable[..] specialized with ParamSpec) - # TODO: Should be `Unknown` - reveal_type(annotated_unknown) # revealed: T@AnnotatedType - # TODO: Should be `Unknown | None` - reveal_type(optional_unknown) # revealed: T@MyOptional | None + reveal_type(annotated_unknown) # revealed: Unknown + reveal_type(optional_unknown) # revealed: Unknown | None ``` For a type variable with a default, we use the default type: @@ -563,10 +549,13 @@ MyListWithDefault = list[T_default] def _( list_of_str: MyListWithDefault[str], list_of_int: MyListWithDefault, + list_of_str_or_none: MyListWithDefault[str] | None, + list_of_int_or_none: MyListWithDefault | None, ): reveal_type(list_of_str) # revealed: list[str] - # TODO: this should be `list[int]` - reveal_type(list_of_int) # revealed: list[T_default@MyListWithDefault] + reveal_type(list_of_int) # revealed: list[int] + reveal_type(list_of_str_or_none) # revealed: list[str] | None + reveal_type(list_of_int_or_none) # revealed: list[int] | None ``` (Generic) implicit type aliases can be used as base classes: @@ -601,7 +590,7 @@ Generic implicit type aliases can be imported from other modules and specialized ```py from typing_extensions import TypeVar -T = TypeVar("T") +T = TypeVar("T", default=str) MyList = list[T] ``` @@ -615,9 +604,13 @@ import my_types as mt def _( list_of_ints1: MyList[int], list_of_ints2: mt.MyList[int], + list_of_str: mt.MyList, + list_of_str_or_none: mt.MyList | None, ): reveal_type(list_of_ints1) # revealed: list[int] reveal_type(list_of_ints2) # revealed: list[int] + reveal_type(list_of_str) # revealed: list[str] + reveal_type(list_of_str_or_none) # revealed: list[str] | None ``` ### In stringified annotations diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 2cafdcfcdd..f957c2193c 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -8263,6 +8263,16 @@ impl<'db> Type<'db> { _ => None, } } + + /// Default-specialize all legacy typevars in this type. + /// + /// This is used when an implicit type alias is referenced without explicitly specializing it. + pub(crate) fn default_specialize(self, db: &'db dyn Db) -> Type<'db> { + let mut variables = FxOrderSet::default(); + self.find_legacy_typevars(db, None, &mut variables); + let generic_context = GenericContext::from_typevar_instances(db, variables); + self.apply_specialization(db, generic_context.default_specialization(db, None)) + } } impl<'db> From<&Type<'db>> for Type<'db> { diff --git a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs index 8c211735cd..e143b5c7fa 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs @@ -144,18 +144,19 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ) } _ => TypeAndQualifiers::declared( - ty.in_type_expression( - builder.db(), - builder.scope(), - builder.typevar_binding_context, - ) - .unwrap_or_else(|error| { - error.into_fallback_type( - &builder.context, - annotation, - builder.is_reachable(annotation), + ty.default_specialize(builder.db()) + .in_type_expression( + builder.db(), + builder.scope(), + builder.typevar_binding_context, ) - }), + .unwrap_or_else(|error| { + error.into_fallback_type( + &builder.context, + annotation, + builder.is_reachable(annotation), + ) + }), ), } } diff --git a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs index 0e3a326139..edee2bb5da 100644 --- a/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs +++ b/crates/ty_python_semantic/src/types/infer/builder/type_expression.rs @@ -91,6 +91,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ast::Expr::Name(name) => match name.ctx { ast::ExprContext::Load => self .infer_name_expression(name) + .default_specialize(self.db()) .in_type_expression(self.db(), self.scope(), self.typevar_binding_context) .unwrap_or_else(|error| { error.into_fallback_type( @@ -108,6 +109,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> { ast::Expr::Attribute(attribute_expression) => match attribute_expression.ctx { ast::ExprContext::Load => self .infer_attribute_expression(attribute_expression) + .default_specialize(self.db()) .in_type_expression(self.db(), self.scope(), self.typevar_binding_context) .unwrap_or_else(|error| { error.into_fallback_type(