[ty] Support using legacy typing aliases for generic classes in type annotations (#18404)

Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
This commit is contained in:
lipefree 2025-06-03 13:09:51 +02:00 committed by GitHub
parent 67d94d9ec8
commit f23d2c9b9e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 158 additions and 63 deletions

View File

@ -31,41 +31,81 @@ def f(
ordered_dict_parametrized: typing.OrderedDict[int, str], ordered_dict_parametrized: typing.OrderedDict[int, str],
): ):
reveal_type(list_bare) # revealed: list[Unknown] reveal_type(list_bare) # revealed: list[Unknown]
# TODO: revealed: list[int] reveal_type(list_parametrized) # revealed: list[int]
reveal_type(list_parametrized) # revealed: list[Unknown]
reveal_type(dict_bare) # revealed: dict[Unknown, Unknown] reveal_type(dict_bare) # revealed: dict[Unknown, Unknown]
# TODO: revealed: dict[int, str] reveal_type(dict_parametrized) # revealed: dict[int, str]
reveal_type(dict_parametrized) # revealed: dict[Unknown, Unknown]
reveal_type(set_bare) # revealed: set[Unknown] reveal_type(set_bare) # revealed: set[Unknown]
# TODO: revealed: set[int] reveal_type(set_parametrized) # revealed: set[int]
reveal_type(set_parametrized) # revealed: set[Unknown]
# TODO: revealed: frozenset[Unknown]
reveal_type(frozen_set_bare) # revealed: frozenset[Unknown] reveal_type(frozen_set_bare) # revealed: frozenset[Unknown]
# TODO: revealed: frozenset[str] reveal_type(frozen_set_parametrized) # revealed: frozenset[str]
reveal_type(frozen_set_parametrized) # revealed: frozenset[Unknown]
reveal_type(chain_map_bare) # revealed: ChainMap[Unknown, Unknown] reveal_type(chain_map_bare) # revealed: ChainMap[Unknown, Unknown]
# TODO: revealed: ChainMap[str, int] reveal_type(chain_map_parametrized) # revealed: ChainMap[str, int]
reveal_type(chain_map_parametrized) # revealed: ChainMap[Unknown, Unknown]
reveal_type(counter_bare) # revealed: Counter[Unknown] reveal_type(counter_bare) # revealed: Counter[Unknown]
# TODO: revealed: Counter[int] reveal_type(counter_parametrized) # revealed: Counter[int]
reveal_type(counter_parametrized) # revealed: Counter[Unknown]
reveal_type(default_dict_bare) # revealed: defaultdict[Unknown, Unknown] reveal_type(default_dict_bare) # revealed: defaultdict[Unknown, Unknown]
# TODO: revealed: defaultdict[str, int] reveal_type(default_dict_parametrized) # revealed: defaultdict[str, int]
reveal_type(default_dict_parametrized) # revealed: defaultdict[Unknown, Unknown]
reveal_type(deque_bare) # revealed: deque[Unknown] reveal_type(deque_bare) # revealed: deque[Unknown]
# TODO: revealed: deque[str] reveal_type(deque_parametrized) # revealed: deque[str]
reveal_type(deque_parametrized) # revealed: deque[Unknown]
reveal_type(ordered_dict_bare) # revealed: OrderedDict[Unknown, Unknown] reveal_type(ordered_dict_bare) # revealed: OrderedDict[Unknown, Unknown]
# TODO: revealed: OrderedDict[int, str] reveal_type(ordered_dict_parametrized) # revealed: OrderedDict[int, str]
reveal_type(ordered_dict_parametrized) # revealed: OrderedDict[Unknown, Unknown] ```
## Incorrect number of type arguments
In case the incorrect number of type arguments is passed, a diagnostic is given.
```py
import typing
def f(
# error: [invalid-type-form] "Legacy alias `typing.List` expected exactly 1 argument, got 2"
incorrect_list: typing.List[int, int],
# error: [invalid-type-form] "Legacy alias `typing.Dict` expected exactly 2 arguments, got 3"
incorrect_dict: typing.Dict[int, int, int],
# error: [invalid-type-form] "Legacy alias `typing.Dict` expected exactly 2 arguments, got 1"
incorrect_dict2: typing.Dict[int], # type argument is not a tuple here
# error: [invalid-type-form]
incorrect_set: typing.Set[int, int],
# error: [invalid-type-form]
incorrect_frozen_set: typing.FrozenSet[int, int],
# error: [invalid-type-form]
incorrect_chain_map: typing.ChainMap[int, int, int],
# error: [invalid-type-form]
incorrect_chain_map2: typing.ChainMap[int],
# error: [invalid-type-form]
incorrect_counter: typing.Counter[int, int],
# error: [invalid-type-form]
incorrect_default_dict: typing.DefaultDict[int, int, int],
# error: [invalid-type-form]
incorrect_default_dict2: typing.DefaultDict[int],
# error: [invalid-type-form]
incorrect_deque: typing.Deque[int, int],
# error: [invalid-type-form]
incorrect_ordered_dict: typing.OrderedDict[int, int, int],
# error: [invalid-type-form]
incorrect_ordered_dict2: typing.OrderedDict[int],
):
reveal_type(incorrect_list) # revealed: list[Unknown]
reveal_type(incorrect_dict) # revealed: dict[Unknown, Unknown]
reveal_type(incorrect_dict2) # revealed: dict[Unknown, Unknown]
reveal_type(incorrect_set) # revealed: set[Unknown]
reveal_type(incorrect_frozen_set) # revealed: frozenset[Unknown]
reveal_type(incorrect_chain_map) # revealed: ChainMap[Unknown, Unknown]
reveal_type(incorrect_chain_map2) # revealed: ChainMap[Unknown, Unknown]
reveal_type(incorrect_counter) # revealed: Counter[Unknown]
reveal_type(incorrect_default_dict) # revealed: defaultdict[Unknown, Unknown]
reveal_type(incorrect_default_dict2) # revealed: defaultdict[Unknown, Unknown]
reveal_type(incorrect_deque) # revealed: deque[Unknown]
reveal_type(incorrect_ordered_dict) # revealed: OrderedDict[Unknown, Unknown]
reveal_type(incorrect_ordered_dict2) # revealed: OrderedDict[Unknown, Unknown]
``` ```
## Inheritance ## Inheritance

View File

@ -24,12 +24,13 @@ class dict[K, V, Extra]: ...
def reveal_type(obj, /): ... def reveal_type(obj, /): ...
``` ```
If we don't, then we won't be able to infer the types of variadic keyword arguments correctly. If we don't, then we may get "surprising" results when inferring the types of variadic keyword
arguments.
```py ```py
def f(**kwargs): def f(**kwargs):
reveal_type(kwargs) # revealed: Unknown reveal_type(kwargs) # revealed: dict[Unknown, Unknown, Unknown]
def g(**kwargs: int): def g(**kwargs: int):
reveal_type(kwargs) # revealed: Unknown reveal_type(kwargs) # revealed: dict[Unknown, Unknown, Unknown]
``` ```

View File

@ -2426,7 +2426,7 @@ impl<'db> KnownClass {
return Type::unknown(); return Type::unknown();
}; };
let Some(generic_context) = class_literal.generic_context(db) else { let Some(generic_context) = class_literal.generic_context(db) else {
return Type::unknown(); return Type::instance(db, ClassType::NonGeneric(class_literal));
}; };
let types = specialization.into_iter().collect::<Box<[_]>>(); let types = specialization.into_iter().collect::<Box<[_]>>();
@ -2437,11 +2437,11 @@ impl<'db> KnownClass {
if MESSAGES.lock().unwrap().insert(self) { if MESSAGES.lock().unwrap().insert(self) {
tracing::info!( tracing::info!(
"Wrong number of types when specializing {}. \ "Wrong number of types when specializing {}. \
Falling back to `Unknown` for the symbol instead.", Falling back to default specialization for the symbol instead.",
self.display(db) self.display(db)
); );
} }
return Type::unknown(); return Type::instance(db, class_literal.default_specialization(db));
} }
let specialization = generic_context.specialize(db, types); let specialization = generic_context.specialize(db, types);

View File

@ -7975,7 +7975,7 @@ impl<'db> TypeInferenceBuilder<'db> {
match value_ty { match value_ty {
Type::SpecialForm(SpecialFormType::Annotated) => { Type::SpecialForm(SpecialFormType::Annotated) => {
// This branch is similar to the corresponding branch in `infer_parameterized_known_instance_type_expression`, but // This branch is similar to the corresponding branch in `infer_parameterized_special_form_type_expression`, but
// `Annotated[…]` can appear both in annotation expressions and in type expressions, and needs to be handled slightly // `Annotated[…]` can appear both in annotation expressions and in type expressions, and needs to be handled slightly
// differently in each case (calling either `infer_type_expression_*` or `infer_annotation_expression_*`). // differently in each case (calling either `infer_type_expression_*` or `infer_annotation_expression_*`).
if let ast::Expr::Tuple(ast::ExprTuple { if let ast::Expr::Tuple(ast::ExprTuple {
@ -8701,6 +8701,43 @@ impl<'db> TypeInferenceBuilder<'db> {
} }
} }
fn infer_parameterized_legacy_typing_alias(
&mut self,
subscript_node: &ast::ExprSubscript,
expected_arg_count: usize,
alias: SpecialFormType,
class: KnownClass,
) -> Type<'db> {
let arguments = &*subscript_node.slice;
let (args, args_number) = if let ast::Expr::Tuple(t) = arguments {
(Either::Left(t), t.len())
} else {
(Either::Right([arguments]), 1)
};
if args_number != expected_arg_count {
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript_node) {
let noun = if expected_arg_count == 1 {
"argument"
} else {
"arguments"
};
builder.into_diagnostic(format_args!(
"Legacy alias `{alias}` expected exactly {expected_arg_count} {noun}, \
got {args_number}",
));
}
}
let ty = class.to_specialized_instance(
self.db(),
args.into_iter()
.map(|node| self.infer_type_expression(node)),
);
if arguments.is_tuple_expr() {
self.store_expression_type(arguments, ty);
}
ty
}
fn infer_parameterized_special_form_type_expression( fn infer_parameterized_special_form_type_expression(
&mut self, &mut self,
subscript: &ast::ExprSubscript, subscript: &ast::ExprSubscript,
@ -8916,43 +8953,60 @@ impl<'db> TypeInferenceBuilder<'db> {
} }
}, },
// TODO: Generics SpecialFormType::ChainMap => self.infer_parameterized_legacy_typing_alias(
SpecialFormType::ChainMap => { subscript,
self.infer_type_expression(arguments_slice); 2,
KnownClass::ChainMap.to_instance(db) SpecialFormType::ChainMap,
} KnownClass::ChainMap,
SpecialFormType::OrderedDict => { ),
self.infer_type_expression(arguments_slice); SpecialFormType::OrderedDict => self.infer_parameterized_legacy_typing_alias(
KnownClass::OrderedDict.to_instance(db) subscript,
} 2,
SpecialFormType::Dict => { SpecialFormType::OrderedDict,
self.infer_type_expression(arguments_slice); KnownClass::OrderedDict,
KnownClass::Dict.to_instance(db) ),
} SpecialFormType::Dict => self.infer_parameterized_legacy_typing_alias(
SpecialFormType::List => { subscript,
self.infer_type_expression(arguments_slice); 2,
KnownClass::List.to_instance(db) SpecialFormType::Dict,
} KnownClass::Dict,
SpecialFormType::DefaultDict => { ),
self.infer_type_expression(arguments_slice); SpecialFormType::List => self.infer_parameterized_legacy_typing_alias(
KnownClass::DefaultDict.to_instance(db) subscript,
} 1,
SpecialFormType::Counter => { SpecialFormType::List,
self.infer_type_expression(arguments_slice); KnownClass::List,
KnownClass::Counter.to_instance(db) ),
} SpecialFormType::DefaultDict => self.infer_parameterized_legacy_typing_alias(
SpecialFormType::Set => { subscript,
self.infer_type_expression(arguments_slice); 2,
KnownClass::Set.to_instance(db) SpecialFormType::DefaultDict,
} KnownClass::DefaultDict,
SpecialFormType::FrozenSet => { ),
self.infer_type_expression(arguments_slice); SpecialFormType::Counter => self.infer_parameterized_legacy_typing_alias(
KnownClass::FrozenSet.to_instance(db) subscript,
} 1,
SpecialFormType::Deque => { SpecialFormType::Counter,
self.infer_type_expression(arguments_slice); KnownClass::Counter,
KnownClass::Deque.to_instance(db) ),
} SpecialFormType::Set => self.infer_parameterized_legacy_typing_alias(
subscript,
1,
SpecialFormType::Set,
KnownClass::Set,
),
SpecialFormType::FrozenSet => self.infer_parameterized_legacy_typing_alias(
subscript,
1,
SpecialFormType::FrozenSet,
KnownClass::FrozenSet,
),
SpecialFormType::Deque => self.infer_parameterized_legacy_typing_alias(
subscript,
1,
SpecialFormType::Deque,
KnownClass::Deque,
),
SpecialFormType::ReadOnly => { SpecialFormType::ReadOnly => {
self.infer_type_expression(arguments_slice); self.infer_type_expression(arguments_slice);