[ty] Infer types for key-based access on TypedDicts (#19763)
## Summary
This PR adds type inference for key-based access on `TypedDict`s and a
new diagnostic for invalid subscript accesses:
```py
class Person(TypedDict):
name: str
age: int | None
alice = Person(name="Alice", age=25)
reveal_type(alice["name"]) # revealed: str
reveal_type(alice["age"]) # revealed: int | None
alice["naem"] # Unknown key "naem" - did you mean "name"?
```
## Test Plan
Updated Markdown tests
This commit is contained in:
@@ -244,8 +244,7 @@ class D(TypedDict):
|
||||
|
||||
td = D(x=1, label="a")
|
||||
td["x"] = 0
|
||||
# TODO: should be Literal[0]
|
||||
reveal_type(td["x"]) # revealed: @Todo(Support for `TypedDict`)
|
||||
reveal_type(td["x"]) # revealed: Literal[0]
|
||||
|
||||
# error: [unresolved-reference]
|
||||
does["not"]["exist"] = 0
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: typed_dict.md - `TypedDict` - Diagnostics
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/typed_dict.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing import TypedDict, Final
|
||||
2 |
|
||||
3 | class Person(TypedDict):
|
||||
4 | name: str
|
||||
5 | age: int | None
|
||||
6 |
|
||||
7 | def access_invalid_literal_string_key(person: Person):
|
||||
8 | person["naem"] # error: [invalid-key]
|
||||
9 |
|
||||
10 | NAME_KEY: Final = "naem"
|
||||
11 |
|
||||
12 | def access_invalid_key(person: Person):
|
||||
13 | person[NAME_KEY] # error: [invalid-key]
|
||||
14 |
|
||||
15 | def access_with_str_key(person: Person, str_key: str):
|
||||
16 | person[str_key] # error: [invalid-key]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-key]: Invalid key access on TypedDict `Person`
|
||||
--> src/mdtest_snippet.py:8:5
|
||||
|
|
||||
7 | def access_invalid_literal_string_key(person: Person):
|
||||
8 | person["naem"] # error: [invalid-key]
|
||||
| ------ ^^^^^^ Unknown key "naem" - did you mean "name"?
|
||||
| |
|
||||
| TypedDict `Person`
|
||||
9 |
|
||||
10 | NAME_KEY: Final = "naem"
|
||||
|
|
||||
info: rule `invalid-key` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-key]: Invalid key access on TypedDict `Person`
|
||||
--> src/mdtest_snippet.py:13:5
|
||||
|
|
||||
12 | def access_invalid_key(person: Person):
|
||||
13 | person[NAME_KEY] # error: [invalid-key]
|
||||
| ------ ^^^^^^^^ Unknown key "naem" - did you mean "name"?
|
||||
| |
|
||||
| TypedDict `Person`
|
||||
14 |
|
||||
15 | def access_with_str_key(person: Person, str_key: str):
|
||||
|
|
||||
info: rule `invalid-key` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-key]: TypedDict `Person` cannot be indexed with a key of type `str`
|
||||
--> src/mdtest_snippet.py:16:12
|
||||
|
|
||||
15 | def access_with_str_key(person: Person, str_key: str):
|
||||
16 | person[str_key] # error: [invalid-key]
|
||||
| ^^^^^^^
|
||||
|
|
||||
info: rule `invalid-key` is enabled by default
|
||||
|
||||
```
|
||||
@@ -25,12 +25,21 @@ alice: Person = {"name": "Alice", "age": 30}
|
||||
reveal_type(alice["name"]) # revealed: Unknown
|
||||
# TODO: this should be `int | None`
|
||||
reveal_type(alice["age"]) # revealed: Unknown
|
||||
|
||||
# TODO: this should reveal `Unknown`, and it should emit an error
|
||||
reveal_type(alice["non_existing"]) # revealed: Unknown
|
||||
```
|
||||
|
||||
Inhabitants can also be created through a constructor call:
|
||||
|
||||
```py
|
||||
bob = Person(name="Bob", age=25)
|
||||
|
||||
reveal_type(bob["name"]) # revealed: str
|
||||
reveal_type(bob["age"]) # revealed: int | None
|
||||
|
||||
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "non_existing""
|
||||
reveal_type(bob["non_existing"]) # revealed: Unknown
|
||||
```
|
||||
|
||||
Methods that are available on `dict`s are also available on `TypedDict`s:
|
||||
@@ -127,6 +136,39 @@ dangerous(alice)
|
||||
reveal_type(alice["name"]) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Key-based access
|
||||
|
||||
```py
|
||||
from typing import TypedDict, Final, Literal, Any
|
||||
|
||||
class Person(TypedDict):
|
||||
name: str
|
||||
age: int | None
|
||||
|
||||
NAME_FINAL: Final = "name"
|
||||
AGE_FINAL: Final[Literal["age"]] = "age"
|
||||
|
||||
def _(person: Person, literal_key: Literal["age"], union_of_keys: Literal["age", "name"], str_key: str, unknown_key: Any) -> None:
|
||||
reveal_type(person["name"]) # revealed: str
|
||||
reveal_type(person["age"]) # revealed: int | None
|
||||
|
||||
reveal_type(person[NAME_FINAL]) # revealed: str
|
||||
reveal_type(person[AGE_FINAL]) # revealed: int | None
|
||||
|
||||
reveal_type(person[literal_key]) # revealed: int | None
|
||||
|
||||
reveal_type(person[union_of_keys]) # revealed: int | None | str
|
||||
|
||||
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "non_existing""
|
||||
reveal_type(person["non_existing"]) # revealed: Unknown
|
||||
|
||||
# error: [invalid-key] "TypedDict `Person` cannot be indexed with a key of type `str`"
|
||||
reveal_type(person[str_key]) # revealed: Unknown
|
||||
|
||||
# No error here:
|
||||
reveal_type(person[unknown_key]) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Methods on `TypedDict`
|
||||
|
||||
```py
|
||||
@@ -333,4 +375,29 @@ reveal_type(Message.__required_keys__) # revealed: @Todo(Support for `TypedDict
|
||||
msg.content
|
||||
```
|
||||
|
||||
## Diagnostics
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
Snapshot tests for diagnostic messages including suggestions:
|
||||
|
||||
```py
|
||||
from typing import TypedDict, Final
|
||||
|
||||
class Person(TypedDict):
|
||||
name: str
|
||||
age: int | None
|
||||
|
||||
def access_invalid_literal_string_key(person: Person):
|
||||
person["naem"] # error: [invalid-key]
|
||||
|
||||
NAME_KEY: Final = "naem"
|
||||
|
||||
def access_invalid_key(person: Person):
|
||||
person[NAME_KEY] # error: [invalid-key]
|
||||
|
||||
def access_with_str_key(person: Person, str_key: str):
|
||||
person[str_key] # error: [invalid-key]
|
||||
```
|
||||
|
||||
[`typeddict`]: https://typing.python.org/en/latest/spec/typeddict.html
|
||||
|
||||
Reference in New Issue
Block a user