mirror of https://github.com/astral-sh/ruff
[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 ✔️ 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 😎 ## Test Plan Updated pre-existing tests and add a few new ones.
This commit is contained in:
parent
c5b8d551df
commit
e6ddeed386
|
|
@ -190,14 +190,10 @@ def _(
|
||||||
reveal_type(type_of_str_or_int) # revealed: type[str] | int
|
reveal_type(type_of_str_or_int) # revealed: type[str] | int
|
||||||
reveal_type(int_or_callable) # revealed: int | ((str, /) -> bytes)
|
reveal_type(int_or_callable) # revealed: int | ((str, /) -> bytes)
|
||||||
reveal_type(callable_or_int) # revealed: ((str, /) -> bytes) | int
|
reveal_type(callable_or_int) # revealed: ((str, /) -> bytes) | int
|
||||||
# TODO should be Unknown | int
|
reveal_type(type_var_or_int) # revealed: Unknown | int
|
||||||
reveal_type(type_var_or_int) # revealed: T@TypeVarOrInt | int
|
reveal_type(int_or_type_var) # revealed: int | Unknown
|
||||||
# TODO should be int | Unknown
|
reveal_type(type_var_or_none) # revealed: Unknown | None
|
||||||
reveal_type(int_or_type_var) # revealed: int | T@IntOrTypeVar
|
reveal_type(none_or_type_var) # revealed: None | Unknown
|
||||||
# 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If a type is unioned with itself in a value expression, the result is just that type. No
|
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,
|
annotated_unknown: AnnotatedType,
|
||||||
optional_unknown: MyOptional,
|
optional_unknown: MyOptional,
|
||||||
):
|
):
|
||||||
# TODO: This should be `list[Unknown]`
|
reveal_type(list_unknown) # revealed: list[Unknown]
|
||||||
reveal_type(list_unknown) # revealed: list[T@MyList]
|
reveal_type(dict_unknown) # revealed: dict[Unknown, Unknown]
|
||||||
# TODO: This should be `dict[Unknown, Unknown]`
|
reveal_type(subclass_of_unknown) # revealed: type[Unknown]
|
||||||
reveal_type(dict_unknown) # revealed: dict[T@MyDict, U@MyDict]
|
reveal_type(int_and_unknown) # revealed: tuple[int, Unknown]
|
||||||
# TODO: Should be `type[Unknown]`
|
reveal_type(pair_of_unknown) # revealed: tuple[Unknown, Unknown]
|
||||||
reveal_type(subclass_of_unknown) # revealed: type[T@MyType]
|
reveal_type(unknown_and_unknown) # revealed: tuple[Unknown, Unknown]
|
||||||
# TODO: Should be `tuple[int, Unknown]`
|
reveal_type(list_or_tuple) # revealed: list[Unknown] | tuple[Unknown, ...]
|
||||||
reveal_type(int_and_unknown) # revealed: tuple[int, T@IntAndType]
|
reveal_type(list_or_tuple_legacy) # revealed: list[Unknown] | tuple[Unknown, ...]
|
||||||
# TODO: Should be `tuple[Unknown, Unknown]`
|
# TODO: should be (...) -> 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(my_callable) # revealed: @Todo(Callable[..] specialized with ParamSpec)
|
reveal_type(my_callable) # revealed: @Todo(Callable[..] specialized with ParamSpec)
|
||||||
# TODO: Should be `Unknown`
|
reveal_type(annotated_unknown) # revealed: Unknown
|
||||||
reveal_type(annotated_unknown) # revealed: T@AnnotatedType
|
reveal_type(optional_unknown) # revealed: Unknown | None
|
||||||
# TODO: Should be `Unknown | None`
|
|
||||||
reveal_type(optional_unknown) # revealed: T@MyOptional | None
|
|
||||||
```
|
```
|
||||||
|
|
||||||
For a type variable with a default, we use the default type:
|
For a type variable with a default, we use the default type:
|
||||||
|
|
@ -563,10 +549,13 @@ MyListWithDefault = list[T_default]
|
||||||
def _(
|
def _(
|
||||||
list_of_str: MyListWithDefault[str],
|
list_of_str: MyListWithDefault[str],
|
||||||
list_of_int: MyListWithDefault,
|
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]
|
reveal_type(list_of_str) # revealed: list[str]
|
||||||
# TODO: this should be `list[int]`
|
reveal_type(list_of_int) # revealed: list[int]
|
||||||
reveal_type(list_of_int) # revealed: list[T_default@MyListWithDefault]
|
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:
|
(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
|
```py
|
||||||
from typing_extensions import TypeVar
|
from typing_extensions import TypeVar
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T", default=str)
|
||||||
|
|
||||||
MyList = list[T]
|
MyList = list[T]
|
||||||
```
|
```
|
||||||
|
|
@ -615,9 +604,13 @@ import my_types as mt
|
||||||
def _(
|
def _(
|
||||||
list_of_ints1: MyList[int],
|
list_of_ints1: MyList[int],
|
||||||
list_of_ints2: mt.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_ints1) # revealed: list[int]
|
||||||
reveal_type(list_of_ints2) # 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
|
### In stringified annotations
|
||||||
|
|
|
||||||
|
|
@ -8263,6 +8263,16 @@ impl<'db> Type<'db> {
|
||||||
_ => None,
|
_ => 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> {
|
impl<'db> From<&Type<'db>> for Type<'db> {
|
||||||
|
|
|
||||||
|
|
@ -144,7 +144,8 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_ => TypeAndQualifiers::declared(
|
_ => TypeAndQualifiers::declared(
|
||||||
ty.in_type_expression(
|
ty.default_specialize(builder.db())
|
||||||
|
.in_type_expression(
|
||||||
builder.db(),
|
builder.db(),
|
||||||
builder.scope(),
|
builder.scope(),
|
||||||
builder.typevar_binding_context,
|
builder.typevar_binding_context,
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||||
ast::Expr::Name(name) => match name.ctx {
|
ast::Expr::Name(name) => match name.ctx {
|
||||||
ast::ExprContext::Load => self
|
ast::ExprContext::Load => self
|
||||||
.infer_name_expression(name)
|
.infer_name_expression(name)
|
||||||
|
.default_specialize(self.db())
|
||||||
.in_type_expression(self.db(), self.scope(), self.typevar_binding_context)
|
.in_type_expression(self.db(), self.scope(), self.typevar_binding_context)
|
||||||
.unwrap_or_else(|error| {
|
.unwrap_or_else(|error| {
|
||||||
error.into_fallback_type(
|
error.into_fallback_type(
|
||||||
|
|
@ -108,6 +109,7 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||||
ast::Expr::Attribute(attribute_expression) => match attribute_expression.ctx {
|
ast::Expr::Attribute(attribute_expression) => match attribute_expression.ctx {
|
||||||
ast::ExprContext::Load => self
|
ast::ExprContext::Load => self
|
||||||
.infer_attribute_expression(attribute_expression)
|
.infer_attribute_expression(attribute_expression)
|
||||||
|
.default_specialize(self.db())
|
||||||
.in_type_expression(self.db(), self.scope(), self.typevar_binding_context)
|
.in_type_expression(self.db(), self.scope(), self.typevar_binding_context)
|
||||||
.unwrap_or_else(|error| {
|
.unwrap_or_else(|error| {
|
||||||
error.into_fallback_type(
|
error.into_fallback_type(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue