mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-17 03:18:38 +00:00
Merge b2d6f35366 into 0d2cd84df4
This commit is contained in:
commit
604ca32178
9 changed files with 572 additions and 50 deletions
|
|
@ -482,17 +482,14 @@ class TD2(TypedDict):
|
|||
x: str
|
||||
|
||||
def f(self, dt: dict[str, Any], key: str):
|
||||
# TODO: This should not error once typed dict assignability is implemented.
|
||||
# error: [invalid-assignment]
|
||||
x1: TD = dt.get(key, {})
|
||||
reveal_type(x1) # revealed: TD
|
||||
reveal_type(x1) # revealed: Any
|
||||
|
||||
x2: TD = dt.get(key, {"x": 0})
|
||||
reveal_type(x2) # revealed: Any
|
||||
|
||||
x3: TD | None = dt.get(key, {})
|
||||
# TODO: This should reveal `Any` once typed dict assignability is implemented.
|
||||
reveal_type(x3) # revealed: Any | None
|
||||
reveal_type(x3) # revealed: Any
|
||||
|
||||
x4: TD | None = dt.get(key, {"x": 0})
|
||||
reveal_type(x4) # revealed: Any
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ import dict_a
|
|||
import dict_b
|
||||
|
||||
def _(b_person: dict_b.Person):
|
||||
# TODO should be error: [invalid-assignment] "Object of type `dict_b.Person` is not assignable to `dict_a.Person`"
|
||||
# error: [invalid-assignment] "Object of type `dict_b.Person` is not assignable to `dict_a.Person`"
|
||||
person_var: dict_a.Person = b_person
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ source: crates/ty_test/src/lib.rs
|
|||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Unknown key for all elemens of a union
|
||||
mdtest name: assignment_diagnostics.md - Subscript assignment diagnostics - Unknown key for all elements of a union
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_diagnostics.md
|
||||
---
|
||||
|
||||
|
|
@ -16,26 +16,27 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia
|
|||
2 |
|
||||
3 | class Person(TypedDict):
|
||||
4 | name: str
|
||||
5 |
|
||||
6 | class Animal(TypedDict):
|
||||
7 | name: str
|
||||
8 | legs: int
|
||||
9 |
|
||||
10 | def _(being: Person | Animal) -> None:
|
||||
11 | # error: [invalid-key]
|
||||
5 | phone_number: str
|
||||
6 |
|
||||
7 | class Animal(TypedDict):
|
||||
8 | name: str
|
||||
9 | legs: int
|
||||
10 |
|
||||
11 | def _(being: Person | Animal) -> None:
|
||||
12 | # error: [invalid-key]
|
||||
13 | being["surname"] = "unknown"
|
||||
13 | # error: [invalid-key]
|
||||
14 | being["surname"] = "unknown"
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-key]: Invalid key for TypedDict `Person`
|
||||
--> src/mdtest_snippet.py:13:5
|
||||
--> src/mdtest_snippet.py:14:5
|
||||
|
|
||||
11 | # error: [invalid-key]
|
||||
12 | # error: [invalid-key]
|
||||
13 | being["surname"] = "unknown"
|
||||
13 | # error: [invalid-key]
|
||||
14 | being["surname"] = "unknown"
|
||||
| ----- ^^^^^^^^^ Unknown key "surname" - did you mean "name"?
|
||||
| |
|
||||
| TypedDict `Person` in union type `Person | Animal`
|
||||
|
|
@ -46,11 +47,11 @@ info: rule `invalid-key` is enabled by default
|
|||
|
||||
```
|
||||
error[invalid-key]: Invalid key for TypedDict `Animal`
|
||||
--> src/mdtest_snippet.py:13:5
|
||||
--> src/mdtest_snippet.py:14:5
|
||||
|
|
||||
11 | # error: [invalid-key]
|
||||
12 | # error: [invalid-key]
|
||||
13 | being["surname"] = "unknown"
|
||||
13 | # error: [invalid-key]
|
||||
14 | being["surname"] = "unknown"
|
||||
| ----- ^^^^^^^^^ Unknown key "surname" - did you mean "name"?
|
||||
| |
|
||||
| TypedDict `Animal` in union type `Person | Animal`
|
||||
|
|
@ -16,23 +16,24 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/subscript/assignment_dia
|
|||
2 |
|
||||
3 | class Person(TypedDict):
|
||||
4 | name: str
|
||||
5 |
|
||||
6 | class Animal(TypedDict):
|
||||
7 | name: str
|
||||
8 | legs: int
|
||||
9 |
|
||||
10 | def _(being: Person | Animal) -> None:
|
||||
11 | being["legs"] = 4 # error: [invalid-key]
|
||||
5 | phone_number: str
|
||||
6 |
|
||||
7 | class Animal(TypedDict):
|
||||
8 | name: str
|
||||
9 | legs: int
|
||||
10 |
|
||||
11 | def _(being: Person | Animal) -> None:
|
||||
12 | being["legs"] = 4 # error: [invalid-key]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-key]: Invalid key for TypedDict `Person`
|
||||
--> src/mdtest_snippet.py:11:5
|
||||
--> src/mdtest_snippet.py:12:5
|
||||
|
|
||||
10 | def _(being: Person | Animal) -> None:
|
||||
11 | being["legs"] = 4 # error: [invalid-key]
|
||||
11 | def _(being: Person | Animal) -> None:
|
||||
12 | being["legs"] = 4 # error: [invalid-key]
|
||||
| ----- ^^^^^^ Unknown key "legs"
|
||||
| |
|
||||
| TypedDict `Person` in union type `Person | Animal`
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ from typing import TypedDict
|
|||
|
||||
class Person(TypedDict):
|
||||
name: str
|
||||
phone_number: str
|
||||
|
||||
class Animal(TypedDict):
|
||||
name: str
|
||||
|
|
@ -86,13 +87,14 @@ def _(being: Person | Animal) -> None:
|
|||
being["legs"] = 4 # error: [invalid-key]
|
||||
```
|
||||
|
||||
## Unknown key for all elemens of a union
|
||||
## Unknown key for all elements of a union
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
|
||||
class Person(TypedDict):
|
||||
name: str
|
||||
phone_number: str
|
||||
|
||||
class Animal(TypedDict):
|
||||
name: str
|
||||
|
|
|
|||
|
|
@ -460,6 +460,7 @@ and their types, rather than the class hierarchy:
|
|||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
from typing_extensions import ReadOnly
|
||||
|
||||
class Person(TypedDict):
|
||||
name: str
|
||||
|
|
@ -468,10 +469,117 @@ class Employee(TypedDict):
|
|||
name: str
|
||||
employee_id: int
|
||||
|
||||
p1: Person = Employee(name="Alice", employee_id=1)
|
||||
class Robot(TypedDict):
|
||||
name: int
|
||||
|
||||
# TODO: this should be an error
|
||||
e1: Employee = Person(name="Eve")
|
||||
_: Person = Employee(name="Alice", employee_id=1)
|
||||
|
||||
# error: [invalid-assignment] "Object of type `Person` is not assignable to `Employee`"
|
||||
_: Employee = Person(name="Eve")
|
||||
|
||||
# error: [invalid-assignment] "Object of type `Robot` is not assignable to `Person`"
|
||||
_: Person = Robot(name=0xDEADC0DE)
|
||||
# error: [invalid-assignment] "Object of type `Person` is not assignable to `Robot`"
|
||||
_: Robot = Person(name="Del Spooner")
|
||||
```
|
||||
|
||||
Required keys in the target must also be required in the source. This includes explicitly
|
||||
`NotRequired` keys and also all the keys in a `TypedDict` with `total=False`. If a key is
|
||||
`NotRequired` and also mutable in the target, then it must be `NotRequired` in the source too
|
||||
(because `del` is allowed on that key the target):
|
||||
|
||||
```py
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
class Spy(TypedDict):
|
||||
name: NotRequired[str]
|
||||
|
||||
# invalid because `Spy` might be missing `name`
|
||||
# error: [invalid-assignment] "Object of type `Spy` is not assignable to `Person`"
|
||||
_: Person = Spy(name="Powers, Austin Powers")
|
||||
|
||||
# invalid because `Spy` is allowed to delete `name`, while `Person` is not
|
||||
# error: [invalid-assignment] "Object of type `Person` is not assignable to `Spy`"
|
||||
_: Spy = Person(name="Bond, James Bond")
|
||||
|
||||
class Amnesiac(TypedDict, total=False):
|
||||
name: ReadOnly[str]
|
||||
|
||||
# invalid because `Amnesiac` might be missing `name`
|
||||
# error: [invalid-assignment] "Object of type `Amnesiac` is not assignable to `Person`"
|
||||
_: Person = Amnesiac()
|
||||
|
||||
# Allowed. `Amnesiac` can't delete `name`, because it's read-only.
|
||||
_: Amnesiac = Person(name="Jason Bourne")
|
||||
```
|
||||
|
||||
If the target item is read-only, then the source item can have any assignable type. But if the
|
||||
target is mutable, the source type must match exactly. The required and not-required cases are
|
||||
different codepaths, so we need test all the permutations:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import ReadOnly
|
||||
|
||||
class RequiredMutableInt(TypedDict):
|
||||
x: int
|
||||
|
||||
class RequiredReadOnlyInt(TypedDict):
|
||||
x: ReadOnly[int]
|
||||
|
||||
class NotRequiredMutableInt(TypedDict):
|
||||
x: NotRequired[int]
|
||||
|
||||
class NotRequiredReadOnlyInt(TypedDict):
|
||||
x: NotRequired[ReadOnly[int]]
|
||||
|
||||
class RequiredMutableBool(TypedDict):
|
||||
x: bool
|
||||
|
||||
class RequiredReadOnlyBool(TypedDict):
|
||||
x: ReadOnly[bool]
|
||||
|
||||
class NotRequiredMutableBool(TypedDict):
|
||||
x: NotRequired[bool]
|
||||
|
||||
class NotRequiredReadOnlyBool(TypedDict):
|
||||
x: NotRequired[ReadOnly[bool]]
|
||||
|
||||
_: RequiredMutableInt = RequiredMutableInt(x=1)
|
||||
_: RequiredMutableInt = RequiredReadOnlyInt(x=1) # error: [invalid-assignment]
|
||||
_: RequiredMutableInt = NotRequiredMutableInt(x=1) # error: [invalid-assignment]
|
||||
_: RequiredMutableInt = NotRequiredReadOnlyInt(x=1) # error: [invalid-assignment]
|
||||
_: RequiredMutableInt = RequiredMutableBool(x=True) # error: [invalid-assignment]
|
||||
_: RequiredMutableInt = RequiredReadOnlyBool(x=True) # error: [invalid-assignment]
|
||||
_: RequiredMutableInt = NotRequiredMutableBool(x=True) # error: [invalid-assignment]
|
||||
_: RequiredMutableInt = NotRequiredReadOnlyBool(x=True) # error: [invalid-assignment]
|
||||
|
||||
_: RequiredReadOnlyInt = RequiredMutableInt(x=1)
|
||||
_: RequiredReadOnlyInt = RequiredReadOnlyInt(x=1)
|
||||
_: RequiredReadOnlyInt = NotRequiredMutableInt(x=1) # error: [invalid-assignment]
|
||||
_: RequiredReadOnlyInt = NotRequiredReadOnlyInt(x=1) # error: [invalid-assignment]
|
||||
_: RequiredReadOnlyInt = RequiredMutableBool(x=True)
|
||||
_: RequiredReadOnlyInt = RequiredReadOnlyBool(x=True)
|
||||
_: RequiredReadOnlyInt = NotRequiredMutableBool(x=True) # error: [invalid-assignment]
|
||||
_: RequiredReadOnlyInt = NotRequiredReadOnlyBool(x=True) # error: [invalid-assignment]
|
||||
|
||||
_: NotRequiredMutableInt = RequiredMutableInt(x=1) # error: [invalid-assignment]
|
||||
_: NotRequiredMutableInt = RequiredReadOnlyInt(x=1) # error: [invalid-assignment]
|
||||
_: NotRequiredMutableInt = NotRequiredMutableInt(x=1)
|
||||
_: NotRequiredMutableInt = NotRequiredReadOnlyInt(x=1) # error: [invalid-assignment]
|
||||
_: NotRequiredMutableInt = RequiredMutableBool(x=True) # error: [invalid-assignment]
|
||||
_: NotRequiredMutableInt = RequiredReadOnlyBool(x=True) # error: [invalid-assignment]
|
||||
_: NotRequiredMutableInt = NotRequiredMutableBool(x=True) # error: [invalid-assignment]
|
||||
_: NotRequiredMutableInt = NotRequiredReadOnlyBool(x=True) # error: [invalid-assignment]
|
||||
|
||||
_: NotRequiredReadOnlyInt = RequiredMutableInt(x=1)
|
||||
_: NotRequiredReadOnlyInt = RequiredReadOnlyInt(x=1)
|
||||
_: NotRequiredReadOnlyInt = NotRequiredMutableInt(x=1)
|
||||
_: NotRequiredReadOnlyInt = NotRequiredReadOnlyInt(x=1)
|
||||
_: NotRequiredReadOnlyInt = RequiredMutableBool(x=True)
|
||||
_: NotRequiredReadOnlyInt = RequiredReadOnlyBool(x=True)
|
||||
_: NotRequiredReadOnlyInt = NotRequiredMutableBool(x=True)
|
||||
_: NotRequiredReadOnlyInt = NotRequiredReadOnlyBool(x=True)
|
||||
```
|
||||
|
||||
All typed dictionaries can be assigned to `Mapping[str, object]`:
|
||||
|
|
@ -483,10 +591,20 @@ class Person(TypedDict):
|
|||
name: str
|
||||
age: int | None
|
||||
|
||||
m: Mapping[str, object] = Person(name="Alice", age=30)
|
||||
alice = Person(name="Alice", age=30)
|
||||
# Always assignable.
|
||||
_: Mapping[str, object] = alice
|
||||
# Follows from above.
|
||||
_: Mapping[str, Any] = alice
|
||||
# Not assignable.
|
||||
# error: [invalid-assignment] "Object of type `Person` is not assignable to `Mapping[str, int]`"
|
||||
_: Mapping[str, int] = alice
|
||||
# TODO: Could be assignable once we support `closed=True` and/or `extra_items`.
|
||||
# error: [invalid-assignment]
|
||||
_: Mapping[str, str | int | None] = alice
|
||||
```
|
||||
|
||||
They can *not* be assigned to `dict[str, object]`, as that would allow them to be mutated in unsafe
|
||||
They *cannot* be assigned to `dict[str, object]`, as that would allow them to be mutated in unsafe
|
||||
ways:
|
||||
|
||||
```py
|
||||
|
|
@ -500,7 +618,7 @@ class Person(TypedDict):
|
|||
|
||||
alice: Person = {"name": "Alice"}
|
||||
|
||||
# TODO: this should be an invalid-assignment error
|
||||
# error: [invalid-argument-type] "Argument to function `dangerous` is incorrect: Expected `dict[str, object]`, found `Person`"
|
||||
dangerous(alice)
|
||||
|
||||
reveal_type(alice["name"]) # revealed: str
|
||||
|
|
@ -515,6 +633,138 @@ alice: dict[str, str] = {"name": "Alice"}
|
|||
alice: Person = alice
|
||||
```
|
||||
|
||||
## A subtle interaction between two structural assignability rules prevents unsoundness
|
||||
|
||||
> For the purposes of these conditions, an open `TypedDict` is treated as if it had **read-only**
|
||||
> extra items of type `object`.
|
||||
|
||||
That language is at the top of [subtyping section of the `TypedDict` spec][subtyping section]. It
|
||||
sounds like an obscure technicality, especially since `extra_items` is still TODO, but it has an
|
||||
important interaction with another rule:
|
||||
|
||||
> For each item in [the destination type]...If it is non-required...If it is mutable...If \[the
|
||||
> source type does not have an item with the same key and also\] has extra items, the extra items
|
||||
> type **must not be read-only**...
|
||||
|
||||
In other words, by default (`closed=False`) a `TypedDict` cannot be assigned to a different
|
||||
`TypedDict` that has an additional, optional, mutable item. That implicit rule turns out to be the
|
||||
only thing standing in the way of this unsound example:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypedDict, NotRequired
|
||||
|
||||
class C(TypedDict):
|
||||
x: int
|
||||
y: str
|
||||
|
||||
class B(TypedDict):
|
||||
x: int
|
||||
|
||||
class A(TypedDict):
|
||||
x: int
|
||||
y: NotRequired[object] # incompatible with both C and (surprisingly!) B
|
||||
|
||||
def b_from_c(c: C) -> B:
|
||||
return c # allowed
|
||||
|
||||
def a_from_b(b: B) -> A:
|
||||
# error: [invalid-return-type] "Return type does not match returned value: expected `A`, found `B`"
|
||||
return b
|
||||
|
||||
# The [invalid-return-type] error above is the only thing that keeps us from corrupting the type of c['y'].
|
||||
c: C = {"x": 1, "y": "hello"}
|
||||
a: A = a_from_b(b_from_c(c))
|
||||
a["y"] = 42
|
||||
```
|
||||
|
||||
If the additional, optional item in the target is read-only, the requirements are *somewhat*
|
||||
relaxed. In this case, because the source might contain have undeclared extra items of any type, the
|
||||
target item must be assignable from `object`:
|
||||
|
||||
```py
|
||||
from typing_extensions import ReadOnly
|
||||
|
||||
class A2(TypedDict):
|
||||
x: int
|
||||
y: NotRequired[ReadOnly[object]]
|
||||
|
||||
def a2_from_b(b: B) -> A2:
|
||||
return b # allowed
|
||||
|
||||
class A3(TypedDict):
|
||||
x: int
|
||||
y: NotRequired[ReadOnly[int]] # not assignable from `object`
|
||||
|
||||
def a3_from_b(b: B) -> A3:
|
||||
return b # error: [invalid-return-type]
|
||||
```
|
||||
|
||||
## Structural assignability supports `TypedDict`s that contain other `TypedDict`s
|
||||
|
||||
```py
|
||||
from typing_extensions import TypedDict, ReadOnly, NotRequired
|
||||
|
||||
class Inner1(TypedDict):
|
||||
name: str
|
||||
|
||||
class Inner2(TypedDict):
|
||||
name: str
|
||||
|
||||
class Outer1(TypedDict):
|
||||
a: Inner1
|
||||
b: ReadOnly[Inner1]
|
||||
c: NotRequired[Inner1]
|
||||
d: ReadOnly[NotRequired[Inner1]]
|
||||
|
||||
class Outer2(TypedDict):
|
||||
a: Inner2
|
||||
b: ReadOnly[Inner2]
|
||||
c: NotRequired[Inner2]
|
||||
d: ReadOnly[NotRequired[Inner2]]
|
||||
|
||||
def _(o1: Outer1, o2: Outer2):
|
||||
_: Outer1 = o2
|
||||
_: Outer2 = o1
|
||||
```
|
||||
|
||||
This also extends to gradual types:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class Inner3(TypedDict):
|
||||
name: Any
|
||||
|
||||
class Outer3(TypedDict):
|
||||
a: Inner3
|
||||
b: ReadOnly[Inner3]
|
||||
c: NotRequired[Inner3]
|
||||
d: ReadOnly[NotRequired[Inner3]]
|
||||
|
||||
class Outer4(TypedDict):
|
||||
a: Any
|
||||
b: ReadOnly[Any]
|
||||
c: NotRequired[Any]
|
||||
d: ReadOnly[NotRequired[Any]]
|
||||
|
||||
def _(o1: Outer1, o2: Outer2, o3: Outer3, o4: Outer4):
|
||||
_: Outer1 = o3
|
||||
_: Outer1 = o4
|
||||
|
||||
_: Outer2 = o3
|
||||
_: Outer2 = o4
|
||||
|
||||
_: Outer3 = o1
|
||||
_: Outer3 = o2
|
||||
_: Outer3 = o3
|
||||
_: Outer3 = o4
|
||||
|
||||
_: Outer4 = o1
|
||||
_: Outer4 = o2
|
||||
_: Outer4 = o3
|
||||
_: Outer4 = o4
|
||||
```
|
||||
|
||||
## Key-based access
|
||||
|
||||
### Reading
|
||||
|
|
@ -561,10 +811,10 @@ def _(
|
|||
|
||||
reveal_type(being["name"]) # revealed: str
|
||||
|
||||
# TODO: A type of `int | None | Unknown` might be better here. The `str` is mixed in
|
||||
# because `Animal.__getitem__` can only return `str`.
|
||||
# TODO: A type of `int | None | Unknown` might be better here. `str` is because
|
||||
# `Person | Animal` reduces to `Animal`, and `Animal.__getitem__` can only return `str`.
|
||||
# error: [invalid-key] "Invalid key for TypedDict `Animal`"
|
||||
reveal_type(being["age"]) # revealed: int | None | str
|
||||
reveal_type(being["age"]) # revealed: str
|
||||
```
|
||||
|
||||
### Writing
|
||||
|
|
@ -835,8 +1085,7 @@ def combine(p: Person, e: Employee):
|
|||
reveal_type(p | p) # revealed: Person
|
||||
reveal_type(e | e) # revealed: Employee
|
||||
|
||||
# TODO: Should be `Person` once we support subtyping for TypedDicts
|
||||
reveal_type(p | e) # revealed: Person | Employee
|
||||
reveal_type(p | e) # revealed: Person
|
||||
```
|
||||
|
||||
When inheriting from a `TypedDict` with a different `total` setting, inherited fields maintain their
|
||||
|
|
@ -997,6 +1246,20 @@ nested: Node = {"name": "n1", "parent": {"name": "n2", "parent": {"name": "n3",
|
|||
nested_invalid: Node = {"name": "n1", "parent": {"name": "n2", "parent": {"name": 3, "parent": None}}}
|
||||
```
|
||||
|
||||
Structural assignment works for recursive `TypedDict`s too:
|
||||
|
||||
```py
|
||||
class Person(TypedDict):
|
||||
name: str
|
||||
parent: Person | None
|
||||
|
||||
def _(node: Node, person: Person):
|
||||
_: Person = node
|
||||
_: Node = person
|
||||
|
||||
_: Node = Person(name="Alice", parent=Node(name="Bob", parent=Person(name="Charlie", parent=None)))
|
||||
```
|
||||
|
||||
## Function/assignment syntax
|
||||
|
||||
This is not yet supported. Make sure that we do not emit false positives for this syntax:
|
||||
|
|
@ -1165,4 +1428,5 @@ reveal_type(actual_td) # revealed: ActualTypedDict
|
|||
reveal_type(actual_td["name"]) # revealed: str
|
||||
```
|
||||
|
||||
[subtyping section]: https://typing.python.org/en/latest/spec/typeddict.html#subtyping-between-typeddict-types
|
||||
[`typeddict`]: https://typing.python.org/en/latest/spec/typeddict.html
|
||||
|
|
|
|||
|
|
@ -2091,11 +2091,14 @@ impl<'db> Type<'db> {
|
|||
ConstraintSet::from(false)
|
||||
}
|
||||
|
||||
(Type::TypedDict(_), _) => {
|
||||
// TODO: Implement assignability and subtyping for TypedDict
|
||||
ConstraintSet::from(relation.is_assignability())
|
||||
}
|
||||
|
||||
(Type::TypedDict(self_typeddict), _) => self_typeddict.has_relation_to_impl(
|
||||
db,
|
||||
target,
|
||||
inferable,
|
||||
relation,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
),
|
||||
// A non-`TypedDict` cannot subtype a `TypedDict`
|
||||
(_, Type::TypedDict(_)) => ConstraintSet::from(false),
|
||||
|
||||
|
|
|
|||
|
|
@ -3932,6 +3932,7 @@ pub enum KnownClass {
|
|||
SupportsIndex,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Mapping,
|
||||
// typing_extensions
|
||||
ExtensionsTypeVar, // must be distinct from typing.TypeVar, backports new features
|
||||
// Collections
|
||||
|
|
@ -4044,6 +4045,7 @@ impl KnownClass {
|
|||
| Self::ABCMeta
|
||||
| Self::Iterable
|
||||
| Self::Iterator
|
||||
| Self::Mapping
|
||||
// Evaluating `NotImplementedType` in a boolean context was deprecated in Python 3.9
|
||||
// and raises a `TypeError` in Python >=3.14
|
||||
// (see https://docs.python.org/3/library/constants.html#NotImplemented)
|
||||
|
|
@ -4128,6 +4130,7 @@ impl KnownClass {
|
|||
| KnownClass::SupportsIndex
|
||||
| KnownClass::Iterable
|
||||
| KnownClass::Iterator
|
||||
| KnownClass::Mapping
|
||||
| KnownClass::ChainMap
|
||||
| KnownClass::Counter
|
||||
| KnownClass::DefaultDict
|
||||
|
|
@ -4211,6 +4214,7 @@ impl KnownClass {
|
|||
| KnownClass::SupportsIndex
|
||||
| KnownClass::Iterable
|
||||
| KnownClass::Iterator
|
||||
| KnownClass::Mapping
|
||||
| KnownClass::ChainMap
|
||||
| KnownClass::Counter
|
||||
| KnownClass::DefaultDict
|
||||
|
|
@ -4294,6 +4298,7 @@ impl KnownClass {
|
|||
| KnownClass::SupportsIndex
|
||||
| KnownClass::Iterable
|
||||
| KnownClass::Iterator
|
||||
| KnownClass::Mapping
|
||||
| KnownClass::ChainMap
|
||||
| KnownClass::Counter
|
||||
| KnownClass::DefaultDict
|
||||
|
|
@ -4406,7 +4411,8 @@ impl KnownClass {
|
|||
| Self::BuiltinFunctionType
|
||||
| Self::ProtocolMeta
|
||||
| Self::Template
|
||||
| KnownClass::Path => false,
|
||||
| Self::Path
|
||||
| Self::Mapping => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4479,6 +4485,7 @@ impl KnownClass {
|
|||
| KnownClass::SupportsIndex
|
||||
| KnownClass::Iterable
|
||||
| KnownClass::Iterator
|
||||
| KnownClass::Mapping
|
||||
| KnownClass::ChainMap
|
||||
| KnownClass::Counter
|
||||
| KnownClass::DefaultDict
|
||||
|
|
@ -4567,6 +4574,7 @@ impl KnownClass {
|
|||
Self::Super => "super",
|
||||
Self::Iterable => "Iterable",
|
||||
Self::Iterator => "Iterator",
|
||||
Self::Mapping => "Mapping",
|
||||
// For example, `typing.List` is defined as `List = _Alias()` in typeshed
|
||||
Self::StdlibAlias => "_Alias",
|
||||
// This is the name the type of `sys.version_info` has in typeshed,
|
||||
|
|
@ -4863,6 +4871,7 @@ impl KnownClass {
|
|||
| Self::StdlibAlias
|
||||
| Self::Iterable
|
||||
| Self::Iterator
|
||||
| Self::Mapping
|
||||
| Self::ProtocolMeta
|
||||
| Self::SupportsIndex => KnownModule::Typing,
|
||||
Self::TypeAliasType
|
||||
|
|
@ -4990,6 +4999,7 @@ impl KnownClass {
|
|||
| Self::InitVar
|
||||
| Self::Iterable
|
||||
| Self::Iterator
|
||||
| Self::Mapping
|
||||
| Self::NamedTupleFallback
|
||||
| Self::NamedTupleLike
|
||||
| Self::ConstraintSet
|
||||
|
|
@ -5078,6 +5088,7 @@ impl KnownClass {
|
|||
| Self::InitVar
|
||||
| Self::Iterable
|
||||
| Self::Iterator
|
||||
| Self::Mapping
|
||||
| Self::NamedTupleFallback
|
||||
| Self::NamedTupleLike
|
||||
| Self::ConstraintSet
|
||||
|
|
@ -5139,6 +5150,7 @@ impl KnownClass {
|
|||
"TypeVar" => &[Self::TypeVar, Self::ExtensionsTypeVar],
|
||||
"Iterable" => &[Self::Iterable],
|
||||
"Iterator" => &[Self::Iterator],
|
||||
"Mapping" => &[Self::Mapping],
|
||||
"ParamSpec" => &[Self::ParamSpec],
|
||||
"ParamSpecArgs" => &[Self::ParamSpecArgs],
|
||||
"ParamSpecKwargs" => &[Self::ParamSpecKwargs],
|
||||
|
|
@ -5276,6 +5288,7 @@ impl KnownClass {
|
|||
| Self::TypeVarTuple
|
||||
| Self::Iterable
|
||||
| Self::Iterator
|
||||
| Self::Mapping
|
||||
| Self::ProtocolMeta
|
||||
| Self::NewType => matches!(module, KnownModule::Typing | KnownModule::TypingExtensions),
|
||||
Self::Deprecated => matches!(module, KnownModule::Warnings | KnownModule::TypingExtensions),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,11 @@ use super::diagnostic::{
|
|||
report_missing_typed_dict_key,
|
||||
};
|
||||
use super::{ApplyTypeMappingVisitor, Type, TypeMapping, visitor};
|
||||
use crate::types::TypeContext;
|
||||
use crate::types::constraints::ConstraintSet;
|
||||
use crate::types::generics::InferableTypeVars;
|
||||
use crate::types::{
|
||||
HasRelationToVisitor, IsDisjointVisitor, KnownClass, TypeContext, TypeRelation,
|
||||
};
|
||||
use crate::{Db, FxOrderMap};
|
||||
|
||||
use ordermap::OrderSet;
|
||||
|
|
@ -76,6 +80,243 @@ impl<'db> TypedDictType<'db> {
|
|||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn has_relation_to_impl(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
target: Type<'db>,
|
||||
inferable: InferableTypeVars<'_, 'db>,
|
||||
relation: TypeRelation<'db>,
|
||||
relation_visitor: &HasRelationToVisitor<'db>,
|
||||
disjointness_visitor: &IsDisjointVisitor<'db>,
|
||||
) -> ConstraintSet<'db> {
|
||||
if let Type::TypedDict(target_typed_dict) = target {
|
||||
self.has_relation_to_other_typeddict_impl(
|
||||
db,
|
||||
target_typed_dict,
|
||||
inferable,
|
||||
relation,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
)
|
||||
} else {
|
||||
Self::has_relation_to_non_typeddict_impl(db, target)
|
||||
}
|
||||
}
|
||||
|
||||
// Subtyping between `TypedDict`s follows the algorithm described at:
|
||||
// https://typing.python.org/en/latest/spec/typeddict.html#subtyping-between-typeddict-types
|
||||
fn has_relation_to_other_typeddict_impl(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
target: TypedDictType<'db>,
|
||||
inferable: InferableTypeVars<'_, 'db>,
|
||||
relation: TypeRelation<'db>,
|
||||
relation_visitor: &HasRelationToVisitor<'db>,
|
||||
disjointness_visitor: &IsDisjointVisitor<'db>,
|
||||
) -> ConstraintSet<'db> {
|
||||
let self_items = self.items(db);
|
||||
let target_items = target.items(db);
|
||||
// Many rules violations short-circuit with "never", but asking whether one field is
|
||||
// [relation] to/of another can produce more complicated constraints, and we collect those.
|
||||
let mut constraints = ConstraintSet::from(true);
|
||||
for (target_item_name, target_item_field) in &target_items {
|
||||
let field_constraints = if target_item_field.is_required() {
|
||||
// required target fields
|
||||
let Some(self_item_field) = self_items.get(target_item_name) else {
|
||||
// Self is missing a required field.
|
||||
return ConstraintSet::from(false);
|
||||
};
|
||||
if !self_item_field.is_required() {
|
||||
// A required field is not required in self.
|
||||
return ConstraintSet::from(false);
|
||||
}
|
||||
relation_visitor.visit(
|
||||
(
|
||||
self_item_field.declared_ty,
|
||||
target_item_field.declared_ty,
|
||||
relation,
|
||||
),
|
||||
|| {
|
||||
if target_item_field.is_read_only() {
|
||||
// For `ReadOnly[]` fields in the target, the corresponding fields in
|
||||
// self need to have the same assignability/subtyping/etc relation
|
||||
// individually that we're looking for overall between the
|
||||
// `TypedDict`s.
|
||||
self_item_field.declared_ty.has_relation_to_impl(
|
||||
db,
|
||||
target_item_field.declared_ty,
|
||||
inferable,
|
||||
relation,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
)
|
||||
} else {
|
||||
if self_item_field.is_read_only() {
|
||||
// A read-only field can't be assigned to a mutable target.
|
||||
return ConstraintSet::from(false);
|
||||
}
|
||||
// For mutable fields in the target, the relation needs to apply both
|
||||
// ways, or else mutating the target could violate the structural
|
||||
// invariants of self. For fully-static types, this is "equivalence".
|
||||
// For gradual types, it depends on the relation, but mutual
|
||||
// assignability is "consistency".
|
||||
self_item_field
|
||||
.declared_ty
|
||||
.has_relation_to_impl(
|
||||
db,
|
||||
target_item_field.declared_ty,
|
||||
inferable,
|
||||
relation,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
)
|
||||
.intersect(
|
||||
db,
|
||||
target_item_field.declared_ty.has_relation_to_impl(
|
||||
db,
|
||||
self_item_field.declared_ty,
|
||||
inferable,
|
||||
relation,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
} else {
|
||||
// `NotRequired[]` target fields
|
||||
if target_item_field.is_read_only() {
|
||||
// As above, for `NotRequired[]` + `ReadOnly[]` fields in the target. It's
|
||||
// tempting to refactor things and unify some of these calls to
|
||||
// `has_relation_to_impl`, but this branch will get more complicated when we
|
||||
// add support for `closed` and `extra_items` (which is why the rules in the
|
||||
// spec are structured like they are), and following the structure of the spec
|
||||
// makes it easier to check the logic here.
|
||||
if let Some(self_item_field) = self_items.get(target_item_name) {
|
||||
relation_visitor.visit(
|
||||
(
|
||||
self_item_field.declared_ty,
|
||||
target_item_field.declared_ty,
|
||||
relation,
|
||||
),
|
||||
|| {
|
||||
self_item_field.declared_ty.has_relation_to_impl(
|
||||
db,
|
||||
target_item_field.declared_ty,
|
||||
inferable,
|
||||
relation,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
// Self is missing this not-required, read-only item. However, since all
|
||||
// `TypeDict`s by default are allowed to have "extra items" of any type
|
||||
// (until we support `closed` and explicit `extra_items`), this key could
|
||||
// actually turn out to have a value. To make sure this is type-safe, the
|
||||
// not-required field in the target needs to be assignable from `object`.
|
||||
// TODO: `closed` and `extra_items` support will go here.
|
||||
KnownClass::Object.to_instance(db).when_assignable_to(
|
||||
db,
|
||||
target_item_field.declared_ty,
|
||||
inferable,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// As above, for `NotRequired[]` mutable fields in the target. Again the logic
|
||||
// is largely the same for now, but it will get more complicated with `closed`
|
||||
// and `extra_items`.
|
||||
if let Some(self_item_field) = self_items.get(target_item_name) {
|
||||
if self_item_field.is_read_only() {
|
||||
// A read-only field can't be assigned to a mutable target.
|
||||
return ConstraintSet::from(false);
|
||||
}
|
||||
if self_item_field.is_required() {
|
||||
// A required field can't be assigned to a not-required, mutable field
|
||||
// in the target, because `del` is allowed on the target field.
|
||||
return ConstraintSet::from(false);
|
||||
}
|
||||
relation_visitor.visit(
|
||||
(
|
||||
self_item_field.declared_ty,
|
||||
target_item_field.declared_ty,
|
||||
relation,
|
||||
),
|
||||
|| {
|
||||
// As above, for mutable fields in the target, the relation needs
|
||||
// to apply both ways.
|
||||
self_item_field
|
||||
.declared_ty
|
||||
.has_relation_to_impl(
|
||||
db,
|
||||
target_item_field.declared_ty,
|
||||
inferable,
|
||||
relation,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
)
|
||||
.intersect(
|
||||
db,
|
||||
target_item_field.declared_ty.has_relation_to_impl(
|
||||
db,
|
||||
self_item_field.declared_ty,
|
||||
inferable,
|
||||
relation,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
// Self is missing this not-required, mutable field. This isn't ok if self
|
||||
// has read-only extra items, which all `TypedDict`s effectively do until
|
||||
// we support `closed` and explicit `extra_items`. See "A subtle
|
||||
// interaction between two structural assignability rules prevents
|
||||
// unsoundness" in `typed_dict.md`.
|
||||
// TODO: `closed` and `extra_items` support will go here.
|
||||
ConstraintSet::from(false)
|
||||
}
|
||||
}
|
||||
};
|
||||
constraints.intersect(db, field_constraints);
|
||||
if constraints.is_never_satisfied(db) {
|
||||
return constraints;
|
||||
}
|
||||
}
|
||||
constraints
|
||||
}
|
||||
|
||||
// The only non-`TypedDict` that a `TypedDict` is assignable to is `Mapping[str, object]`.
|
||||
// Although every instance of `TypedDict` is also an instance of `dict[str, object]`, a
|
||||
// `TypedDict` still isn't assignable to `dict`, because mutating the `dict` could break
|
||||
// `TypeDict`'s "structural" invariants.
|
||||
// TODO: When we support `closed` and/or `extra_items`, we could allow assignments to other
|
||||
// compatible `Mapping`s. `extra_items` could also allow for some assignments to `dict`, as
|
||||
// long as `total=False`. (But then again, does anyone want a non-total `TypedDict` where all
|
||||
// key types are a supertype of the extra items type?)
|
||||
fn has_relation_to_non_typeddict_impl(
|
||||
db: &'db dyn Db,
|
||||
target: Type<'db>,
|
||||
) -> ConstraintSet<'db> {
|
||||
debug_assert!(!matches!(target, Type::TypedDict(_)));
|
||||
if let Some(specialization) = target.known_specialization(db, KnownClass::Mapping)
|
||||
&& let &[key_ty, value_ty] = specialization.types(db)
|
||||
// The key type must be exactly `str`.
|
||||
&& key_ty == KnownClass::Str.to_instance(db)
|
||||
// The value type must be `object` (or a gradual type that's consistent with `object`).
|
||||
&& KnownClass::Object
|
||||
.to_instance(db)
|
||||
.is_assignable_to(db, value_ty)
|
||||
{
|
||||
ConstraintSet::from(true)
|
||||
} else {
|
||||
ConstraintSet::from(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn walk_typed_dict_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue