Skip to content

Commit e6ddeed

Browse files
authored
[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 astral-sh/ty#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] != <class '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 `<class 'Bar'>` ``` ## Ecosystem impact Looks like I should have included this last week :sunglasses: ## Test Plan Updated pre-existing tests and add a few new ones.
1 parent c5b8d55 commit e6ddeed

File tree

4 files changed

+49
-43
lines changed

4 files changed

+49
-43
lines changed

crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -190,14 +190,10 @@ def _(
190190
reveal_type(type_of_str_or_int) # revealed: type[str] | int
191191
reveal_type(int_or_callable) # revealed: int | ((str, /) -> bytes)
192192
reveal_type(callable_or_int) # revealed: ((str, /) -> bytes) | int
193-
# TODO should be Unknown | int
194-
reveal_type(type_var_or_int) # revealed: T@TypeVarOrInt | int
195-
# TODO should be int | Unknown
196-
reveal_type(int_or_type_var) # revealed: int | T@IntOrTypeVar
197-
# TODO should be Unknown | None
198-
reveal_type(type_var_or_none) # revealed: T@TypeVarOrNone | None
199-
# TODO should be None | Unknown
200-
reveal_type(none_or_type_var) # revealed: None | T@NoneOrTypeVar
193+
reveal_type(type_var_or_int) # revealed: Unknown | int
194+
reveal_type(int_or_type_var) # revealed: int | Unknown
195+
reveal_type(type_var_or_none) # revealed: Unknown | None
196+
reveal_type(none_or_type_var) # revealed: None | Unknown
201197
```
202198

203199
If a type is unioned with itself in a value expression, the result is just that type. No
@@ -529,28 +525,18 @@ def _(
529525
annotated_unknown: AnnotatedType,
530526
optional_unknown: MyOptional,
531527
):
532-
# TODO: This should be `list[Unknown]`
533-
reveal_type(list_unknown) # revealed: list[T@MyList]
534-
# TODO: This should be `dict[Unknown, Unknown]`
535-
reveal_type(dict_unknown) # revealed: dict[T@MyDict, U@MyDict]
536-
# TODO: Should be `type[Unknown]`
537-
reveal_type(subclass_of_unknown) # revealed: type[T@MyType]
538-
# TODO: Should be `tuple[int, Unknown]`
539-
reveal_type(int_and_unknown) # revealed: tuple[int, T@IntAndType]
540-
# TODO: Should be `tuple[Unknown, Unknown]`
541-
reveal_type(pair_of_unknown) # revealed: tuple[T@Pair, T@Pair]
542-
# TODO: Should be `tuple[Unknown, Unknown]`
543-
reveal_type(unknown_and_unknown) # revealed: tuple[T@Sum, U@Sum]
544-
# TODO: Should be `list[Unknown] | tuple[Unknown, ...]`
545-
reveal_type(list_or_tuple) # revealed: list[T@ListOrTuple] | tuple[T@ListOrTuple, ...]
546-
# TODO: Should be `list[Unknown] | tuple[Unknown, ...]`
547-
reveal_type(list_or_tuple_legacy) # revealed: list[T@ListOrTupleLegacy] | tuple[T@ListOrTupleLegacy, ...]
548-
# TODO: Should be `(...) -> Unknown`
528+
reveal_type(list_unknown) # revealed: list[Unknown]
529+
reveal_type(dict_unknown) # revealed: dict[Unknown, Unknown]
530+
reveal_type(subclass_of_unknown) # revealed: type[Unknown]
531+
reveal_type(int_and_unknown) # revealed: tuple[int, Unknown]
532+
reveal_type(pair_of_unknown) # revealed: tuple[Unknown, Unknown]
533+
reveal_type(unknown_and_unknown) # revealed: tuple[Unknown, Unknown]
534+
reveal_type(list_or_tuple) # revealed: list[Unknown] | tuple[Unknown, ...]
535+
reveal_type(list_or_tuple_legacy) # revealed: list[Unknown] | tuple[Unknown, ...]
536+
# TODO: should be (...) -> Unknown
549537
reveal_type(my_callable) # revealed: @Todo(Callable[..] specialized with ParamSpec)
550-
# TODO: Should be `Unknown`
551-
reveal_type(annotated_unknown) # revealed: T@AnnotatedType
552-
# TODO: Should be `Unknown | None`
553-
reveal_type(optional_unknown) # revealed: T@MyOptional | None
538+
reveal_type(annotated_unknown) # revealed: Unknown
539+
reveal_type(optional_unknown) # revealed: Unknown | None
554540
```
555541

556542
For a type variable with a default, we use the default type:
@@ -563,10 +549,13 @@ MyListWithDefault = list[T_default]
563549
def _(
564550
list_of_str: MyListWithDefault[str],
565551
list_of_int: MyListWithDefault,
552+
list_of_str_or_none: MyListWithDefault[str] | None,
553+
list_of_int_or_none: MyListWithDefault | None,
566554
):
567555
reveal_type(list_of_str) # revealed: list[str]
568-
# TODO: this should be `list[int]`
569-
reveal_type(list_of_int) # revealed: list[T_default@MyListWithDefault]
556+
reveal_type(list_of_int) # revealed: list[int]
557+
reveal_type(list_of_str_or_none) # revealed: list[str] | None
558+
reveal_type(list_of_int_or_none) # revealed: list[int] | None
570559
```
571560

572561
(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
601590
```py
602591
from typing_extensions import TypeVar
603592

604-
T = TypeVar("T")
593+
T = TypeVar("T", default=str)
605594

606595
MyList = list[T]
607596
```
@@ -615,9 +604,13 @@ import my_types as mt
615604
def _(
616605
list_of_ints1: MyList[int],
617606
list_of_ints2: mt.MyList[int],
607+
list_of_str: mt.MyList,
608+
list_of_str_or_none: mt.MyList | None,
618609
):
619610
reveal_type(list_of_ints1) # revealed: list[int]
620611
reveal_type(list_of_ints2) # revealed: list[int]
612+
reveal_type(list_of_str) # revealed: list[str]
613+
reveal_type(list_of_str_or_none) # revealed: list[str] | None
621614
```
622615

623616
### In stringified annotations

crates/ty_python_semantic/src/types.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8263,6 +8263,16 @@ impl<'db> Type<'db> {
82638263
_ => None,
82648264
}
82658265
}
8266+
8267+
/// Default-specialize all legacy typevars in this type.
8268+
///
8269+
/// This is used when an implicit type alias is referenced without explicitly specializing it.
8270+
pub(crate) fn default_specialize(self, db: &'db dyn Db) -> Type<'db> {
8271+
let mut variables = FxOrderSet::default();
8272+
self.find_legacy_typevars(db, None, &mut variables);
8273+
let generic_context = GenericContext::from_typevar_instances(db, variables);
8274+
self.apply_specialization(db, generic_context.default_specialization(db, None))
8275+
}
82668276
}
82678277

82688278
impl<'db> From<&Type<'db>> for Type<'db> {

crates/ty_python_semantic/src/types/infer/builder/annotation_expression.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -144,18 +144,19 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
144144
)
145145
}
146146
_ => TypeAndQualifiers::declared(
147-
ty.in_type_expression(
148-
builder.db(),
149-
builder.scope(),
150-
builder.typevar_binding_context,
151-
)
152-
.unwrap_or_else(|error| {
153-
error.into_fallback_type(
154-
&builder.context,
155-
annotation,
156-
builder.is_reachable(annotation),
147+
ty.default_specialize(builder.db())
148+
.in_type_expression(
149+
builder.db(),
150+
builder.scope(),
151+
builder.typevar_binding_context,
157152
)
158-
}),
153+
.unwrap_or_else(|error| {
154+
error.into_fallback_type(
155+
&builder.context,
156+
annotation,
157+
builder.is_reachable(annotation),
158+
)
159+
}),
159160
),
160161
}
161162
}

crates/ty_python_semantic/src/types/infer/builder/type_expression.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
9191
ast::Expr::Name(name) => match name.ctx {
9292
ast::ExprContext::Load => self
9393
.infer_name_expression(name)
94+
.default_specialize(self.db())
9495
.in_type_expression(self.db(), self.scope(), self.typevar_binding_context)
9596
.unwrap_or_else(|error| {
9697
error.into_fallback_type(
@@ -108,6 +109,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
108109
ast::Expr::Attribute(attribute_expression) => match attribute_expression.ctx {
109110
ast::ExprContext::Load => self
110111
.infer_attribute_expression(attribute_expression)
112+
.default_specialize(self.db())
111113
.in_type_expression(self.db(), self.scope(), self.typevar_binding_context)
112114
.unwrap_or_else(|error| {
113115
error.into_fallback_type(

0 commit comments

Comments
 (0)