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
226 changes: 218 additions & 8 deletions crates/ty_python_semantic/resources/mdtest/implicit_type_aliases.md
Original file line number Diff line number Diff line change
Expand Up @@ -680,8 +680,21 @@ def _(
Invalid uses result in diagnostics:

```py
from typing import Literal

# error: [invalid-type-form]
InvalidSubclass = type[1]
InvalidSubclassOf1 = type[1]

# TODO: This should be an error
InvalidSubclassOfLiteral = type[Literal[42]]

def _(
invalid_subclass_of_1: InvalidSubclassOf1,
invalid_subclass_of_literal: InvalidSubclassOfLiteral,
):
reveal_type(invalid_subclass_of_1) # revealed: type[Unknown]
# TODO: this should be `type[Unknown]` or `Unknown`
reveal_type(invalid_subclass_of_literal) # revealed: <class 'int'>
Comment on lines +686 to +697
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is just a follow-up from a previous PR.

```

### `Type[…]`
Expand Down Expand Up @@ -759,6 +772,178 @@ Invalid uses result in diagnostics:
InvalidSubclass = Type[1]
```

## Other `typing` special forms

The following special forms from the `typing` module are also supported in implicit type aliases:

```py
from typing import List, Dict, Set, FrozenSet, ChainMap, Counter, DefaultDict, Deque, OrderedDict

MyList = List[str]
MySet = Set[str]
MyDict = Dict[str, int]
MyFrozenSet = FrozenSet[str]
MyChainMap = ChainMap[str, int]
MyCounter = Counter[str]
MyDefaultDict = DefaultDict[str, int]
MyDeque = Deque[str]
MyOrderedDict = OrderedDict[str, int]

reveal_type(MyList) # revealed: <class 'list[str]'>
reveal_type(MySet) # revealed: <class 'set[str]'>
reveal_type(MyDict) # revealed: <class 'dict[str, int]'>
reveal_type(MyFrozenSet) # revealed: <class 'frozenset[str]'>
reveal_type(MyChainMap) # revealed: <class 'ChainMap[str, int]'>
reveal_type(MyCounter) # revealed: <class 'Counter[str]'>
reveal_type(MyDefaultDict) # revealed: <class 'defaultdict[str, int]'>
reveal_type(MyDeque) # revealed: <class 'deque[str]'>
reveal_type(MyOrderedDict) # revealed: <class 'OrderedDict[str, int]'>

def _(
my_list: MyList,
my_set: MySet,
my_dict: MyDict,
my_frozen_set: MyFrozenSet,
my_chain_map: MyChainMap,
my_counter: MyCounter,
my_default_dict: MyDefaultDict,
my_deque: MyDeque,
my_ordered_dict: MyOrderedDict,
):
reveal_type(my_list) # revealed: list[str]
reveal_type(my_set) # revealed: set[str]
reveal_type(my_dict) # revealed: dict[str, int]
reveal_type(my_frozen_set) # revealed: frozenset[str]
reveal_type(my_chain_map) # revealed: ChainMap[str, int]
reveal_type(my_counter) # revealed: Counter[str]
reveal_type(my_default_dict) # revealed: defaultdict[str, int]
reveal_type(my_deque) # revealed: deque[str]
reveal_type(my_ordered_dict) # revealed: OrderedDict[str, int]
```

All of them are supported in unions:

```py
NoneOrList = None | List[str]
NoneOrSet = None | Set[str]
NoneOrDict = None | Dict[str, int]
NoneOrFrozenSet = None | FrozenSet[str]
NoneOrChainMap = None | ChainMap[str, int]
NoneOrCounter = None | Counter[str]
NoneOrDefaultDict = None | DefaultDict[str, int]
NoneOrDeque = None | Deque[str]
NoneOrOrderedDict = None | OrderedDict[str, int]

ListOrNone = List[int] | None
SetOrNone = Set[int] | None
DictOrNone = Dict[str, int] | None
FrozenSetOrNone = FrozenSet[int] | None
ChainMapOrNone = ChainMap[str, int] | None
CounterOrNone = Counter[str] | None
DefaultDictOrNone = DefaultDict[str, int] | None
DequeOrNone = Deque[str] | None
OrderedDictOrNone = OrderedDict[str, int] | None

reveal_type(NoneOrList) # revealed: types.UnionType
reveal_type(NoneOrSet) # revealed: types.UnionType
reveal_type(NoneOrDict) # revealed: types.UnionType
reveal_type(NoneOrFrozenSet) # revealed: types.UnionType
reveal_type(NoneOrChainMap) # revealed: types.UnionType
reveal_type(NoneOrCounter) # revealed: types.UnionType
reveal_type(NoneOrDefaultDict) # revealed: types.UnionType
reveal_type(NoneOrDeque) # revealed: types.UnionType
reveal_type(NoneOrOrderedDict) # revealed: types.UnionType

reveal_type(ListOrNone) # revealed: types.UnionType
reveal_type(SetOrNone) # revealed: types.UnionType
reveal_type(DictOrNone) # revealed: types.UnionType
reveal_type(FrozenSetOrNone) # revealed: types.UnionType
reveal_type(ChainMapOrNone) # revealed: types.UnionType
reveal_type(CounterOrNone) # revealed: types.UnionType
reveal_type(DefaultDictOrNone) # revealed: types.UnionType
reveal_type(DequeOrNone) # revealed: types.UnionType
reveal_type(OrderedDictOrNone) # revealed: types.UnionType

def _(
none_or_list: NoneOrList,
none_or_set: NoneOrSet,
none_or_dict: NoneOrDict,
none_or_frozen_set: NoneOrFrozenSet,
none_or_chain_map: NoneOrChainMap,
none_or_counter: NoneOrCounter,
none_or_default_dict: NoneOrDefaultDict,
none_or_deque: NoneOrDeque,
none_or_ordered_dict: NoneOrOrderedDict,
list_or_none: ListOrNone,
set_or_none: SetOrNone,
dict_or_none: DictOrNone,
frozen_set_or_none: FrozenSetOrNone,
chain_map_or_none: ChainMapOrNone,
counter_or_none: CounterOrNone,
default_dict_or_none: DefaultDictOrNone,
deque_or_none: DequeOrNone,
ordered_dict_or_none: OrderedDictOrNone,
):
reveal_type(none_or_list) # revealed: None | list[str]
reveal_type(none_or_set) # revealed: None | set[str]
reveal_type(none_or_dict) # revealed: None | dict[str, int]
reveal_type(none_or_frozen_set) # revealed: None | frozenset[str]
reveal_type(none_or_chain_map) # revealed: None | ChainMap[str, int]
reveal_type(none_or_counter) # revealed: None | Counter[str]
reveal_type(none_or_default_dict) # revealed: None | defaultdict[str, int]
reveal_type(none_or_deque) # revealed: None | deque[str]
reveal_type(none_or_ordered_dict) # revealed: None | OrderedDict[str, int]

reveal_type(list_or_none) # revealed: list[int] | None
reveal_type(set_or_none) # revealed: set[int] | None
reveal_type(dict_or_none) # revealed: dict[str, int] | None
reveal_type(frozen_set_or_none) # revealed: frozenset[int] | None
reveal_type(chain_map_or_none) # revealed: ChainMap[str, int] | None
reveal_type(counter_or_none) # revealed: Counter[str] | None
reveal_type(default_dict_or_none) # revealed: defaultdict[str, int] | None
reveal_type(deque_or_none) # revealed: deque[str] | None
reveal_type(ordered_dict_or_none) # revealed: OrderedDict[str, int] | None
```

Invalid uses result in diagnostics:

```py
from typing import List, Dict

# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
InvalidList = List[1]

# error: [invalid-type-form] "`typing.typing.List` requires exactly one argument"
ListTooManyArgs = List[int, str]

# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
InvalidDict1 = Dict[1, str]

# error: [invalid-type-form] "Int literals are not allowed in this context in a type expression"
InvalidDict2 = Dict[str, 2]

# error: [invalid-type-form] "`typing.typing.Dict` requires exactly two arguments, got 1"
DictTooFewArgs = Dict[str]

# error: [invalid-type-form] "`typing.typing.Dict` requires exactly two arguments, got 3"
DictTooManyArgs = Dict[str, int, float]

def _(
invalid_list: InvalidList,
list_too_many_args: ListTooManyArgs,
invalid_dict1: InvalidDict1,
invalid_dict2: InvalidDict2,
dict_too_few_args: DictTooFewArgs,
dict_too_many_args: DictTooManyArgs,
):
reveal_type(invalid_list) # revealed: list[Unknown]
reveal_type(list_too_many_args) # revealed: list[Unknown]
reveal_type(invalid_dict1) # revealed: dict[Unknown, str]
reveal_type(invalid_dict2) # revealed: dict[str, Unknown]
reveal_type(dict_too_few_args) # revealed: dict[str, Unknown]
reveal_type(dict_too_many_args) # revealed: dict[Unknown, Unknown]
```

## Stringified annotations?

From the [typing spec on type aliases](https://typing.python.org/en/latest/spec/aliases.html):
Expand Down Expand Up @@ -789,22 +974,28 @@ We *do* support stringified annotations if they appear in a position where a typ
syntactically expected:

```py
from typing import Union
from typing import Union, List, Dict

ListOfInts = list["int"]
ListOfInts1 = list["int"]
ListOfInts2 = List["int"]
StrOrStyle = Union[str, "Style"]
SubclassOfStyle = type["Style"]
DictStrToStyle = Dict[str, "Style"]

class Style: ...

def _(
list_of_ints: ListOfInts,
list_of_ints1: ListOfInts1,
list_of_ints2: ListOfInts2,
str_or_style: StrOrStyle,
subclass_of_style: SubclassOfStyle,
dict_str_to_style: DictStrToStyle,
):
reveal_type(list_of_ints) # revealed: list[int]
reveal_type(list_of_ints1) # revealed: list[int]
reveal_type(list_of_ints2) # revealed: list[int]
reveal_type(str_or_style) # revealed: str | Style
reveal_type(subclass_of_style) # revealed: type[Style]
reveal_type(dict_str_to_style) # revealed: dict[str, Style]
```

## Recursive
Expand All @@ -828,8 +1019,27 @@ python-version = "3.12"
```

```py
Recursive = list["Recursive" | None]
from typing import List, Dict

def _(r: Recursive):
reveal_type(r) # revealed: list[Divergent]
RecursiveList1 = list["RecursiveList1" | None]
RecursiveList2 = List["RecursiveList2" | None]
RecursiveDict1 = dict[str, "RecursiveDict1" | None]
RecursiveDict2 = Dict[str, "RecursiveDict2" | None]
RecursiveDict3 = dict["RecursiveDict3", int]
RecursiveDict4 = Dict["RecursiveDict4", int]

def _(
recursive_list1: RecursiveList1,
recursive_list2: RecursiveList2,
recursive_dict1: RecursiveDict1,
recursive_dict2: RecursiveDict2,
recursive_dict3: RecursiveDict3,
recursive_dict4: RecursiveDict4,
):
reveal_type(recursive_list1) # revealed: list[Divergent]
reveal_type(recursive_list2) # revealed: list[Divergent]
reveal_type(recursive_dict1) # revealed: dict[str, Divergent]
reveal_type(recursive_dict2) # revealed: dict[str, Divergent]
reveal_type(recursive_dict3) # revealed: dict[Divergent, int]
reveal_type(recursive_dict4) # revealed: dict[Divergent, int]
```
89 changes: 89 additions & 0 deletions crates/ty_python_semantic/src/types/infer/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10779,6 +10779,95 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
InternedType::new(self.db(), argument_ty),
));
}
// `typing` special forms with a single generic argument
Type::SpecialForm(
special_form @ (SpecialFormType::List
| SpecialFormType::Set
| SpecialFormType::FrozenSet
| SpecialFormType::Counter
| SpecialFormType::Deque),
) => {
let slice_ty = self.infer_type_expression(slice);

let element_ty = if matches!(**slice, ast::Expr::Tuple(_)) {
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
builder.into_diagnostic(format_args!(
"`typing.{}` requires exactly one argument",
special_form.repr()
));
}
Type::unknown()
} else {
slice_ty
};

let class = special_form
.aliased_stdlib_class()
.expect("A known stdlib class is available");

return class
.to_specialized_class_type(self.db(), [element_ty])
.map(Type::from)
.unwrap_or_else(Type::unknown);
}
// `typing` special forms with two generic arguments
Type::SpecialForm(
special_form @ (SpecialFormType::Dict
| SpecialFormType::ChainMap
| SpecialFormType::DefaultDict
| SpecialFormType::OrderedDict),
) => {
let (first_ty, second_ty) = if let ast::Expr::Tuple(ast::ExprTuple {
elts: ref arguments,
..
}) = **slice
{
if arguments.len() != 2 {
if let Some(builder) =
self.context.report_lint(&INVALID_TYPE_FORM, subscript)
{
builder.into_diagnostic(format_args!(
"`typing.{}` requires exactly two arguments, got {}",
special_form.repr(),
arguments.len()
));
}
}

if let [first_expr, second_expr] = &arguments[..] {
let first_ty = self.infer_type_expression(first_expr);
let second_ty = self.infer_type_expression(second_expr);

(first_ty, second_ty)
} else {
for argument in arguments {
self.infer_type_expression(argument);
}

(Type::unknown(), Type::unknown())
}
} else {
let first_ty = self.infer_type_expression(slice);

if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
builder.into_diagnostic(format_args!(
"`typing.{}` requires exactly two arguments, got 1",
special_form.repr()
));
}

(first_ty, Type::unknown())
};

let class = special_form
.aliased_stdlib_class()
.expect("Stdlib class available");

return class
.to_specialized_class_type(self.db(), [first_ty, second_ty])
.map(Type::from)
.unwrap_or_else(Type::unknown);
}
_ => {}
}

Expand Down
Loading