mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-30 23:27:27 +00:00
[ty] Support using legacy typing aliases for generic classes in type annotations (#18404)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / mkdocs (push) Waiting to run
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / mkdocs (push) Waiting to run
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
This commit is contained in:
parent
67d94d9ec8
commit
f23d2c9b9e
4 changed files with 158 additions and 63 deletions
|
@ -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]
|
||||
|
||||
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
|
||||
|
|
|
@ -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]
|
||||
```
|
||||
|
|
|
@ -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<[_]>>();
|
||||
|
@ -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);
|
||||
|
|
|
@ -7975,7 +7975,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 {
|
||||
|
@ -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(
|
||||
&mut self,
|
||||
subscript: &ast::ExprSubscript,
|
||||
|
@ -8916,43 +8953,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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue