Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,41 +31,81 @@ def f(
ordered_dict_parametrized: typing.OrderedDict[int, str],
):
reveal_type(list_bare) # revealed: list[Unknown]
# TODO: revealed: list[int]
reveal_type(list_parametrized) # revealed: list[Unknown]
reveal_type(list_parametrized) # revealed: list[int]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add an example of each of these aliases being specialized with the wrong number of arguments?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I will add them !


reveal_type(dict_bare) # revealed: dict[Unknown, Unknown]
# TODO: revealed: dict[int, str]
reveal_type(dict_parametrized) # revealed: dict[Unknown, Unknown]
reveal_type(dict_parametrized) # revealed: dict[int, str]

reveal_type(set_bare) # revealed: set[Unknown]
# TODO: revealed: set[int]
reveal_type(set_parametrized) # revealed: set[Unknown]
reveal_type(set_parametrized) # revealed: set[int]

# TODO: revealed: frozenset[Unknown]
reveal_type(frozen_set_bare) # revealed: frozenset[Unknown]
# TODO: revealed: frozenset[str]
reveal_type(frozen_set_parametrized) # revealed: frozenset[Unknown]
reveal_type(frozen_set_parametrized) # revealed: frozenset[str]

reveal_type(chain_map_bare) # revealed: ChainMap[Unknown, Unknown]
# TODO: revealed: ChainMap[str, int]
reveal_type(chain_map_parametrized) # revealed: ChainMap[Unknown, Unknown]
reveal_type(chain_map_parametrized) # revealed: ChainMap[str, int]

reveal_type(counter_bare) # revealed: Counter[Unknown]
# TODO: revealed: Counter[int]
reveal_type(counter_parametrized) # revealed: Counter[Unknown]
reveal_type(counter_parametrized) # revealed: Counter[int]

reveal_type(default_dict_bare) # revealed: defaultdict[Unknown, Unknown]
# TODO: revealed: defaultdict[str, int]
reveal_type(default_dict_parametrized) # revealed: defaultdict[Unknown, Unknown]
reveal_type(default_dict_parametrized) # revealed: defaultdict[str, int]

reveal_type(deque_bare) # revealed: deque[Unknown]
# TODO: revealed: deque[str]
reveal_type(deque_parametrized) # revealed: deque[Unknown]
reveal_type(deque_parametrized) # revealed: deque[str]

reveal_type(ordered_dict_bare) # revealed: OrderedDict[Unknown, Unknown]
# TODO: revealed: OrderedDict[int, str]
reveal_type(ordered_dict_parametrized) # revealed: OrderedDict[Unknown, Unknown]
reveal_type(ordered_dict_parametrized) # revealed: OrderedDict[int, str]
```

## 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ class dict[K, V, Extra]: ...
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
def f(**kwargs):
reveal_type(kwargs) # revealed: Unknown
reveal_type(kwargs) # revealed: dict[Unknown, Unknown, Unknown]

def g(**kwargs: int):
reveal_type(kwargs) # revealed: Unknown
reveal_type(kwargs) # revealed: dict[Unknown, Unknown, Unknown]
```
6 changes: 3 additions & 3 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2426,7 +2426,7 @@ impl<'db> KnownClass {
return Type::unknown();
};
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<[_]>>();
Expand All @@ -2437,11 +2437,11 @@ impl<'db> KnownClass {
if MESSAGES.lock().unwrap().insert(self) {
tracing::info!(
"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)
);
}
return Type::unknown();
return Type::instance(db, class_literal.default_specialization(db));
}

let specialization = generic_context.specialize(db, types);
Expand Down
130 changes: 92 additions & 38 deletions crates/ty_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7926,7 +7926,7 @@ impl<'db> TypeInferenceBuilder<'db> {

match value_ty {
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
// differently in each case (calling either `infer_type_expression_*` or `infer_annotation_expression_*`).
if let ast::Expr::Tuple(ast::ExprTuple {
Expand Down Expand Up @@ -8652,6 +8652,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)
Comment on lines +8664 to +8666
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think you could also have done this with std::slice::from_ref for the non-tuple case, so that in both cases you would end up with a slice iterator. Then you wouldn't need Either. (That would be a probably-not-actually-noticeable performance improvement, since iterating over an Either requires a conditional check on whether it's Left or Right at each step.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

};
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(
&mut self,
subscript: &ast::ExprSubscript,
Expand Down Expand Up @@ -8867,43 +8904,60 @@ impl<'db> TypeInferenceBuilder<'db> {
}
},

// TODO: Generics
SpecialFormType::ChainMap => {
self.infer_type_expression(arguments_slice);
KnownClass::ChainMap.to_instance(db)
}
SpecialFormType::OrderedDict => {
self.infer_type_expression(arguments_slice);
KnownClass::OrderedDict.to_instance(db)
}
SpecialFormType::Dict => {
self.infer_type_expression(arguments_slice);
KnownClass::Dict.to_instance(db)
}
SpecialFormType::List => {
self.infer_type_expression(arguments_slice);
KnownClass::List.to_instance(db)
}
SpecialFormType::DefaultDict => {
self.infer_type_expression(arguments_slice);
KnownClass::DefaultDict.to_instance(db)
}
SpecialFormType::Counter => {
self.infer_type_expression(arguments_slice);
KnownClass::Counter.to_instance(db)
}
SpecialFormType::Set => {
self.infer_type_expression(arguments_slice);
KnownClass::Set.to_instance(db)
}
SpecialFormType::FrozenSet => {
self.infer_type_expression(arguments_slice);
KnownClass::FrozenSet.to_instance(db)
}
SpecialFormType::Deque => {
self.infer_type_expression(arguments_slice);
KnownClass::Deque.to_instance(db)
}
SpecialFormType::ChainMap => self.infer_parameterized_legacy_typing_alias(
subscript,
2,
SpecialFormType::ChainMap,
KnownClass::ChainMap,
),
SpecialFormType::OrderedDict => self.infer_parameterized_legacy_typing_alias(
subscript,
2,
SpecialFormType::OrderedDict,
KnownClass::OrderedDict,
),
SpecialFormType::Dict => self.infer_parameterized_legacy_typing_alias(
subscript,
2,
SpecialFormType::Dict,
KnownClass::Dict,
),
SpecialFormType::List => self.infer_parameterized_legacy_typing_alias(
subscript,
1,
SpecialFormType::List,
KnownClass::List,
),
SpecialFormType::DefaultDict => self.infer_parameterized_legacy_typing_alias(
subscript,
2,
SpecialFormType::DefaultDict,
KnownClass::DefaultDict,
),
SpecialFormType::Counter => self.infer_parameterized_legacy_typing_alias(
subscript,
1,
SpecialFormType::Counter,
KnownClass::Counter,
),
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 => {
self.infer_type_expression(arguments_slice);
Expand Down
Loading