-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[ty] Infer types for key-based access on TypedDicts #19763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d604e29
d281fbf
8ed6771
574bff2
2f755e9
7ea1295
57368a5
b02e6c6
9bebba2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
|
||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So.. all of these still don't work, because the type of |
||
| ``` | ||
|
|
||
| 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"" | ||
sharkdp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 | ||
sharkdp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ``` | ||
|
|
||
| ## 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: | ||
carljm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ```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 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know that we had a custom Levenshtein implementation in #18705, but that was removed again. And
strsimis already a transitive dependency for the CLI version oftyat least — viaclap.Happy to replace that with something else though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have no opinions here -- I'm in favor of whatever works and is implemented :)
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The advantage of going with the custom implementation in #18705 (which I think it would be fairly easy to bring back -- it's quite isolated as a module) is that Brent and I based it directly on the CPython implementation of this feature, and we brought in most of CPython's tests for this feature. CPython's implementation of this feature is very battle-tested at this point: it's been present in several stable releases of Python and initially received a large number of bug reports (which have since been fixed) regarding bad "Did you mean?" suggestions. So at this point I think we can be pretty confident that CPython's implementation is very well tuned for giving good suggestions for typos in Python code specifically.
Having said that, it's obviously nice for us to have to maintain less code, and exactly which Levenshtein implementation we go with probably isn't the most important issue for us right now :-)