mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-18 11:41:21 +00:00
[ty] dict is not assignable to TypedDict (#21238)
## Summary A lot of the bidirectional inference work relies on `dict` not being assignable to `TypedDict`, so I think it makes sense to add this before fully implementing https://github.com/astral-sh/ty/issues/1387.
This commit is contained in:
parent
42adfd40ea
commit
3c8fb68765
11 changed files with 169 additions and 75 deletions
|
|
@ -76,6 +76,7 @@ def _() -> TD:
|
|||
|
||||
def _() -> TD:
|
||||
# error: [missing-typed-dict-key] "Missing required key 'x' in TypedDict `TD` constructor"
|
||||
# error: [invalid-return-type]
|
||||
return {}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -1685,8 +1685,7 @@ def int_or_str() -> int | str:
|
|||
x = f([{"x": 1}], int_or_str())
|
||||
reveal_type(x) # revealed: int | str
|
||||
|
||||
# TODO: error: [no-matching-overload] "No overload of function `f` matches arguments"
|
||||
# we currently incorrectly consider `list[dict[str, int]]` a subtype of `list[T]`
|
||||
# error: [no-matching-overload] "No overload of function `f` matches arguments"
|
||||
f([{"y": 1}], int_or_str())
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -277,7 +277,6 @@ def _(flag: bool):
|
|||
x = f({"x": 1})
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
# TODO: error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `T`, found `dict[str, int]`"
|
||||
# we currently consider `TypedDict` instances to be subtypes of `dict`
|
||||
# error: [invalid-argument-type] "Argument to function `f` is incorrect: Expected `T`, found `dict[Unknown | str, Unknown | int]`"
|
||||
f({"y": 1})
|
||||
```
|
||||
|
|
|
|||
|
|
@ -162,10 +162,13 @@ The type context is propagated down into the comprehension:
|
|||
class Person(TypedDict):
|
||||
name: str
|
||||
|
||||
# TODO: This should not error.
|
||||
# error: [invalid-assignment]
|
||||
persons: list[Person] = [{"name": n} for n in ["Alice", "Bob"]]
|
||||
reveal_type(persons) # revealed: list[Person]
|
||||
|
||||
# TODO: This should be an error
|
||||
# TODO: This should be an invalid-key error.
|
||||
# error: [invalid-assignment]
|
||||
invalid: list[Person] = [{"misspelled": n} for n in ["Alice", "Bob"]]
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -39,16 +39,19 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/typed_dict.md
|
|||
25 | person[str_key] = "Alice" # error: [invalid-key]
|
||||
26 |
|
||||
27 | def create_with_invalid_string_key():
|
||||
28 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key]
|
||||
29 | bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key]
|
||||
30 | from typing_extensions import ReadOnly
|
||||
31 |
|
||||
32 | class Employee(TypedDict):
|
||||
33 | id: ReadOnly[int]
|
||||
34 | name: str
|
||||
35 |
|
||||
36 | def write_to_readonly_key(employee: Employee):
|
||||
37 | employee["id"] = 42 # error: [invalid-assignment]
|
||||
28 | # error: [invalid-key]
|
||||
29 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"}
|
||||
30 |
|
||||
31 | # error: [invalid-key]
|
||||
32 | bob = Person(name="Bob", age=25, unknown="Bar")
|
||||
33 | from typing_extensions import ReadOnly
|
||||
34 |
|
||||
35 | class Employee(TypedDict):
|
||||
36 | id: ReadOnly[int]
|
||||
37 | name: str
|
||||
38 |
|
||||
39 | def write_to_readonly_key(employee: Employee):
|
||||
40 | employee["id"] = 42 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
|
@ -158,16 +161,17 @@ info: rule `invalid-key` is enabled by default
|
|||
|
||||
```
|
||||
error[invalid-key]: Invalid key for TypedDict `Person`
|
||||
--> src/mdtest_snippet.py:28:21
|
||||
--> src/mdtest_snippet.py:29:21
|
||||
|
|
||||
27 | def create_with_invalid_string_key():
|
||||
28 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key]
|
||||
28 | # error: [invalid-key]
|
||||
29 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"}
|
||||
| -----------------------------^^^^^^^^^--------
|
||||
| | |
|
||||
| | Unknown key "unknown"
|
||||
| TypedDict `Person`
|
||||
29 | bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key]
|
||||
30 | from typing_extensions import ReadOnly
|
||||
30 |
|
||||
31 | # error: [invalid-key]
|
||||
|
|
||||
info: rule `invalid-key` is enabled by default
|
||||
|
||||
|
|
@ -175,13 +179,12 @@ info: rule `invalid-key` is enabled by default
|
|||
|
||||
```
|
||||
error[invalid-key]: Invalid key for TypedDict `Person`
|
||||
--> src/mdtest_snippet.py:29:11
|
||||
--> src/mdtest_snippet.py:32:11
|
||||
|
|
||||
27 | def create_with_invalid_string_key():
|
||||
28 | alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key]
|
||||
29 | bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key]
|
||||
31 | # error: [invalid-key]
|
||||
32 | bob = Person(name="Bob", age=25, unknown="Bar")
|
||||
| ------ TypedDict `Person` ^^^^^^^^^^^^^ Unknown key "unknown"
|
||||
30 | from typing_extensions import ReadOnly
|
||||
33 | from typing_extensions import ReadOnly
|
||||
|
|
||||
info: rule `invalid-key` is enabled by default
|
||||
|
||||
|
|
@ -189,21 +192,21 @@ info: rule `invalid-key` is enabled by default
|
|||
|
||||
```
|
||||
error[invalid-assignment]: Cannot assign to key "id" on TypedDict `Employee`
|
||||
--> src/mdtest_snippet.py:37:5
|
||||
--> src/mdtest_snippet.py:40:5
|
||||
|
|
||||
36 | def write_to_readonly_key(employee: Employee):
|
||||
37 | employee["id"] = 42 # error: [invalid-assignment]
|
||||
39 | def write_to_readonly_key(employee: Employee):
|
||||
40 | employee["id"] = 42 # error: [invalid-assignment]
|
||||
| -------- ^^^^ key is marked read-only
|
||||
| |
|
||||
| TypedDict `Employee`
|
||||
|
|
||||
info: Item declaration
|
||||
--> src/mdtest_snippet.py:33:5
|
||||
--> src/mdtest_snippet.py:36:5
|
||||
|
|
||||
32 | class Employee(TypedDict):
|
||||
33 | id: ReadOnly[int]
|
||||
35 | class Employee(TypedDict):
|
||||
36 | id: ReadOnly[int]
|
||||
| ----------------- Read-only item declared here
|
||||
34 | name: str
|
||||
37 | name: str
|
||||
|
|
||||
info: rule `invalid-assignment` is enabled by default
|
||||
|
||||
|
|
|
|||
|
|
@ -96,29 +96,29 @@ The construction of a `TypedDict` is checked for type correctness:
|
|||
```py
|
||||
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`"
|
||||
eve1a: Person = {"name": b"Eve", "age": None}
|
||||
|
||||
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`"
|
||||
eve1b = Person(name=b"Eve", age=None)
|
||||
|
||||
# TODO should reveal Person (should be fixed by implementing assignability for TypedDicts)
|
||||
reveal_type(eve1a) # revealed: dict[Unknown | str, Unknown | bytes | None]
|
||||
reveal_type(eve1a) # revealed: Person
|
||||
reveal_type(eve1b) # revealed: Person
|
||||
|
||||
# error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor"
|
||||
eve2a: Person = {"age": 22}
|
||||
|
||||
# error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `Person` constructor"
|
||||
eve2b = Person(age=22)
|
||||
|
||||
# TODO should reveal Person (should be fixed by implementing assignability for TypedDicts)
|
||||
reveal_type(eve2a) # revealed: dict[Unknown | str, Unknown | int]
|
||||
reveal_type(eve2a) # revealed: Person
|
||||
reveal_type(eve2b) # revealed: Person
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
|
||||
eve3a: Person = {"name": "Eve", "age": 25, "extra": True}
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
|
||||
eve3b = Person(name="Eve", age=25, extra=True)
|
||||
|
||||
# TODO should reveal Person (should be fixed by implementing assignability for TypedDicts)
|
||||
reveal_type(eve3a) # revealed: dict[Unknown | str, Unknown | str | int]
|
||||
reveal_type(eve3a) # revealed: Person
|
||||
reveal_type(eve3b) # revealed: Person
|
||||
```
|
||||
|
||||
|
|
@ -238,15 +238,19 @@ All of these are missing the required `age` field:
|
|||
```py
|
||||
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
|
||||
alice2: Person = {"name": "Alice"}
|
||||
|
||||
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
|
||||
Person(name="Alice")
|
||||
|
||||
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
|
||||
Person({"name": "Alice"})
|
||||
|
||||
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `Person` constructor"
|
||||
# error: [invalid-argument-type]
|
||||
accepts_person({"name": "Alice"})
|
||||
|
||||
# TODO: this should be an error, similar to the above
|
||||
# TODO: this should be an invalid-key error, similar to the above
|
||||
# error: [invalid-assignment]
|
||||
house.owner = {"name": "Alice"}
|
||||
|
||||
a_person: Person
|
||||
|
|
@ -259,19 +263,25 @@ All of these have an invalid type for the `name` field:
|
|||
```py
|
||||
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
|
||||
alice3: Person = {"name": None, "age": 30}
|
||||
|
||||
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
|
||||
Person(name=None, age=30)
|
||||
|
||||
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
|
||||
Person({"name": None, "age": 30})
|
||||
|
||||
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
|
||||
# error: [invalid-argument-type]
|
||||
accepts_person({"name": None, "age": 30})
|
||||
# TODO: this should be an error, similar to the above
|
||||
|
||||
# TODO: this should be an invalid-key error
|
||||
# error: [invalid-assignment]
|
||||
house.owner = {"name": None, "age": 30}
|
||||
|
||||
a_person: Person
|
||||
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
|
||||
a_person = {"name": None, "age": 30}
|
||||
|
||||
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
|
||||
(a_person := {"name": None, "age": 30})
|
||||
```
|
||||
|
|
@ -281,19 +291,25 @@ All of these have an extra field that is not defined in the `TypedDict`:
|
|||
```py
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
|
||||
alice4: Person = {"name": "Alice", "age": 30, "extra": True}
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
|
||||
Person(name="Alice", age=30, extra=True)
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
|
||||
Person({"name": "Alice", "age": 30, "extra": True})
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
|
||||
# error: [invalid-argument-type]
|
||||
accepts_person({"name": "Alice", "age": 30, "extra": True})
|
||||
# TODO: this should be an error
|
||||
|
||||
# TODO: this should be an invalid-key error
|
||||
# error: [invalid-assignment]
|
||||
house.owner = {"name": "Alice", "age": 30, "extra": True}
|
||||
|
||||
a_person: Person
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
|
||||
a_person = {"name": "Alice", "age": 30, "extra": True}
|
||||
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Person`: Unknown key "extra""
|
||||
(a_person := {"name": "Alice", "age": 30, "extra": True})
|
||||
```
|
||||
|
|
@ -490,6 +506,15 @@ dangerous(alice)
|
|||
reveal_type(alice["name"]) # revealed: str
|
||||
```
|
||||
|
||||
Likewise, `dict`s are not assignable to typed dictionaries:
|
||||
|
||||
```py
|
||||
alice: dict[str, str] = {"name": "Alice"}
|
||||
|
||||
# error: [invalid-assignment] "Object of type `dict[str, str]` is not assignable to `Person`"
|
||||
alice: Person = alice
|
||||
```
|
||||
|
||||
## Key-based access
|
||||
|
||||
### Reading
|
||||
|
|
@ -977,7 +1002,7 @@ class Person(TypedDict):
|
|||
name: str
|
||||
age: int | None
|
||||
|
||||
# TODO: this should be an error
|
||||
# error: [invalid-assignment] "Object of type `MyDict` is not assignable to `Person`"
|
||||
x: Person = MyDict({"name": "Alice", "age": 30})
|
||||
```
|
||||
|
||||
|
|
@ -1029,8 +1054,11 @@ def write_to_non_literal_string_key(person: Person, str_key: str):
|
|||
person[str_key] = "Alice" # error: [invalid-key]
|
||||
|
||||
def create_with_invalid_string_key():
|
||||
alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"} # error: [invalid-key]
|
||||
bob = Person(name="Bob", age=25, unknown="Bar") # error: [invalid-key]
|
||||
# error: [invalid-key]
|
||||
alice: Person = {"name": "Alice", "age": 30, "unknown": "Foo"}
|
||||
|
||||
# error: [invalid-key]
|
||||
bob = Person(name="Bob", age=25, unknown="Bar")
|
||||
```
|
||||
|
||||
Assignment to `ReadOnly` keys:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue