Compare commits
9 Commits
gankra/scr
...
ibraheem/t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b22cc1aa2b | ||
|
|
26c25bec82 | ||
|
|
2949f76b47 | ||
|
|
b753851379 | ||
|
|
f2a25b0fd7 | ||
|
|
6981500d93 | ||
|
|
6fdd4c3334 | ||
|
|
2959ff19bc | ||
|
|
98a0b77174 |
@@ -16,13 +16,16 @@ pub fn heap_size<T: GetSize>(value: &T) -> usize {
|
||||
|
||||
/// An implementation of [`GetSize::get_heap_size`] for [`OrderSet`].
|
||||
pub fn order_set_heap_size<T: GetSize, S>(set: &OrderSet<T, S>) -> usize {
|
||||
(set.capacity() * T::get_stack_size()) + set.iter().map(heap_size).sum::<usize>()
|
||||
let size = set.iter().map(heap_size::<T>).sum::<usize>();
|
||||
size + (set.capacity() * T::get_stack_size())
|
||||
}
|
||||
|
||||
/// An implementation of [`GetSize::get_heap_size`] for [`OrderMap`].
|
||||
pub fn order_map_heap_size<K: GetSize, V: GetSize, S>(map: &OrderMap<K, V, S>) -> usize {
|
||||
(map.capacity() * (K::get_stack_size() + V::get_stack_size()))
|
||||
+ (map.iter())
|
||||
.map(|(k, v)| heap_size(k) + heap_size(v))
|
||||
.sum::<usize>()
|
||||
/// An implementation of [`GetSize::get_heap_size`] for [`OrderSet`].
|
||||
pub fn order_map_heap_size<K: GetSize, V: GetSize, S>(set: &OrderMap<K, V, S>) -> usize {
|
||||
let size = set
|
||||
.iter()
|
||||
.map(|(key, val)| heap_size::<K>(key) + heap_size::<V>(val))
|
||||
.sum::<usize>();
|
||||
|
||||
size + (set.capacity() * <(K, V)>::get_stack_size())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Unsupported special types
|
||||
|
||||
We do not understand the functional syntax for creating `NamedTuple`s, `TypedDict`s or `Enum`s yet.
|
||||
But we also do not emit false positives when these are used in type expressions.
|
||||
We do not understand the functional syntax for creating `NamedTuple`s or `Enum`s yet. But we also do
|
||||
not emit false positives when these are used in type expressions.
|
||||
|
||||
```py
|
||||
import collections
|
||||
@@ -10,9 +10,8 @@ import typing
|
||||
|
||||
MyEnum = enum.Enum("MyEnum", ["foo", "bar", "baz"])
|
||||
MyIntEnum = enum.IntEnum("MyIntEnum", ["foo", "bar", "baz"])
|
||||
MyTypedDict = typing.TypedDict("MyTypedDict", {"foo": int})
|
||||
MyNamedTuple1 = typing.NamedTuple("MyNamedTuple1", [("foo", int)])
|
||||
MyNamedTuple2 = collections.namedtuple("MyNamedTuple2", ["foo"])
|
||||
|
||||
def f(a: MyEnum, b: MyTypedDict, c: MyNamedTuple1, d: MyNamedTuple2): ...
|
||||
def f(a: MyEnum, c: MyNamedTuple1, d: MyNamedTuple2): ...
|
||||
```
|
||||
|
||||
@@ -13,6 +13,8 @@ from typing import TypedDict
|
||||
class Person(TypedDict):
|
||||
name: str
|
||||
age: int | None
|
||||
|
||||
reveal_type(Person) # revealed: <class 'Person'>
|
||||
```
|
||||
|
||||
New inhabitants can be created from dict literals. When accessing keys, the correct types should be
|
||||
@@ -21,6 +23,99 @@ inferred based on the `TypedDict` definition:
|
||||
```py
|
||||
alice: Person = {"name": "Alice", "age": 30}
|
||||
|
||||
reveal_type(alice) # revealed: Person
|
||||
reveal_type(alice["name"]) # revealed: str
|
||||
reveal_type(alice["age"]) # revealed: int | None
|
||||
|
||||
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "non_existing""
|
||||
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:
|
||||
|
||||
```py
|
||||
bob.update(age=26)
|
||||
```
|
||||
|
||||
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)
|
||||
|
||||
# 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)
|
||||
|
||||
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
|
||||
eve3a: Person = {"name": "Eve", "age": 25, "extra": True}
|
||||
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
|
||||
eve3b = Person(name="Eve", age=25, extra=True)
|
||||
```
|
||||
|
||||
Assignments to keys are also validated:
|
||||
|
||||
```py
|
||||
# error: [invalid-assignment] "Invalid assignment to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
|
||||
alice["name"] = None
|
||||
|
||||
# error: [invalid-assignment] "Invalid assignment to key "name" with declared type `str` on TypedDict `Person`: value of type `None`"
|
||||
bob["name"] = None
|
||||
```
|
||||
|
||||
Assignments to non-existing keys are disallowed:
|
||||
|
||||
```py
|
||||
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
|
||||
alice["extra"] = True
|
||||
|
||||
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extra""
|
||||
bob["extra"] = True
|
||||
```
|
||||
|
||||
## Functional
|
||||
|
||||
You can also define a `TypedDict` using the functional syntax:
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
from typing_extensions import Required, NotRequired
|
||||
|
||||
Person = TypedDict("Person", {"name": Required[str], "age": int | None})
|
||||
reveal_type(Person) # revealed: typing.TypedDict
|
||||
```
|
||||
|
||||
The `TypedDict` schema must be passed directly as the second argument:
|
||||
|
||||
```py
|
||||
fields = {"name": str}
|
||||
|
||||
# error: [invalid-argument-type] "Argument is incorrect: Expected `_TypedDictSchema`, found `dict[Unknown | str, Unknown | <class 'str'>]`"
|
||||
Other = TypedDict("Other", fields)
|
||||
```
|
||||
|
||||
New inhabitants can be created from dict literals. When accessing keys, the correct types should be
|
||||
inferred based on the `TypedDict` definition:
|
||||
|
||||
```py
|
||||
alice: Person = {"name": "Alice", "age": 30}
|
||||
|
||||
reveal_type(alice) # revealed: Person
|
||||
reveal_type(alice["name"]) # revealed: str
|
||||
reveal_type(alice["age"]) # revealed: int | None
|
||||
|
||||
@@ -220,6 +315,8 @@ Person(name="Alice", age=30, extra=True) # type: ignore
|
||||
The positional dictionary constructor pattern (used by libraries like strawberry) should work
|
||||
correctly:
|
||||
|
||||
`class.py`:
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
|
||||
@@ -240,6 +337,26 @@ user3 = User({"name": None, "age": 25})
|
||||
user4 = User({"name": "Charlie", "age": 30, "extra": True})
|
||||
```
|
||||
|
||||
`functional.py`:
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
|
||||
User = TypedDict("User", {"name": str, "age": int})
|
||||
|
||||
# Valid usage - all required fields provided
|
||||
user1 = User({"name": "Alice", "age": 30})
|
||||
|
||||
# error: [missing-typed-dict-key] "Missing required key 'age' in TypedDict `User` constructor"
|
||||
user2 = User({"name": "Bob"})
|
||||
|
||||
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `User`: value of type `None`"
|
||||
user3 = User({"name": None, "age": 25})
|
||||
|
||||
# error: [invalid-key] "Invalid key access on TypedDict `User`: Unknown key "extra""
|
||||
user4 = User({"name": "Charlie", "age": 30, "extra": True})
|
||||
```
|
||||
|
||||
## Optional fields with `total=False`
|
||||
|
||||
By default, all fields in a `TypedDict` are required (`total=True`). You can make all fields
|
||||
@@ -277,6 +394,29 @@ Extra fields are still not allowed, even with `total=False`:
|
||||
invalid_extra = OptionalPerson(name="George", extra=True)
|
||||
```
|
||||
|
||||
`total` can also be set with the functional syntax:
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
|
||||
OptionalPerson2 = TypedDict("OptionalPerson2", {"name": str, "age": int | None}, total=False)
|
||||
|
||||
charlie = OptionalPerson2()
|
||||
david = OptionalPerson2(name="David")
|
||||
emily = OptionalPerson2(age=30)
|
||||
frank = OptionalPerson2(name="Frank", age=25)
|
||||
|
||||
# TODO: we could emit an error here, because these fields are not guaranteed to exist
|
||||
reveal_type(charlie["name"]) # revealed: str
|
||||
reveal_type(david["age"]) # revealed: int | None
|
||||
|
||||
# error: [invalid-argument-type] "Invalid argument to key "name" with declared type `str` on TypedDict `OptionalPerson2`"
|
||||
invalid = OptionalPerson2(name=123)
|
||||
|
||||
# error: [invalid-key] "Invalid key access on TypedDict `OptionalPerson2`: Unknown key "extra""
|
||||
invalid_extra = OptionalPerson2(name="George", extra=True)
|
||||
```
|
||||
|
||||
## `Required` and `NotRequired`
|
||||
|
||||
You can have fine-grained control over keys using `Required` and `NotRequired` qualifiers. These
|
||||
@@ -331,6 +471,42 @@ Type validation still applies to all fields when provided:
|
||||
invalid_type = Message(id="not-an-int", content="Hello")
|
||||
```
|
||||
|
||||
`Required`/`NotRequired` can also be set with the functional syntax:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypedDict, Required, NotRequired
|
||||
|
||||
# total=False by default, but id is explicitly Required
|
||||
Message2 = TypedDict("Message2", {"id": Required[int], "content": str, "timestamp": NotRequired[str]}, total=False)
|
||||
|
||||
# total=True by default, but content is explicitly NotRequired
|
||||
User2 = TypedDict("User2", {"name": str, "email": Required[str], "bio": NotRequired[str]})
|
||||
|
||||
# Valid Message2 constructions
|
||||
msg1 = Message2(id=1) # id required, content optional
|
||||
msg2 = Message2(id=2, content="Hello") # both provided
|
||||
msg3 = Message2(id=3, timestamp="2024-01-01") # id required, timestamp optional
|
||||
|
||||
# Valid User2 constructions
|
||||
user1 = User2(name="Alice", email="alice@example.com") # required fields
|
||||
user2 = User2(name="Bob", email="bob@example.com", bio="Developer") # with optional bio
|
||||
|
||||
reveal_type(msg1["id"]) # revealed: int
|
||||
reveal_type(msg1["content"]) # revealed: str
|
||||
reveal_type(user1["name"]) # revealed: str
|
||||
reveal_type(user1["bio"]) # revealed: str
|
||||
|
||||
# error: [missing-typed-dict-key] "Missing required key 'id' in TypedDict `Message2` constructor"
|
||||
invalid_msg = Message2(content="Hello") # Missing required id
|
||||
|
||||
# error: [missing-typed-dict-key] "Missing required key 'name' in TypedDict `User2` constructor"
|
||||
# error: [missing-typed-dict-key] "Missing required key 'email' in TypedDict `User2` constructor"
|
||||
invalid_user = User2(bio="No name provided") # Missing required name and email
|
||||
|
||||
# error: [invalid-argument-type] "Invalid argument to key "id" with declared type `int` on TypedDict `Message2`"
|
||||
invalid_type = Message2(id="not-an-int", content="Hello")
|
||||
```
|
||||
|
||||
## Structural assignability
|
||||
|
||||
Assignability between `TypedDict` types is structural, that is, it is based on the presence of keys
|
||||
@@ -388,6 +564,8 @@ reveal_type(alice["name"]) # revealed: str
|
||||
|
||||
### Reading
|
||||
|
||||
`class.py`:
|
||||
|
||||
```py
|
||||
from typing import TypedDict, Final, Literal, Any
|
||||
|
||||
@@ -419,8 +597,41 @@ def _(person: Person, literal_key: Literal["age"], union_of_keys: Literal["age",
|
||||
reveal_type(person[unknown_key]) # revealed: Unknown
|
||||
```
|
||||
|
||||
`functional.py`:
|
||||
|
||||
```py
|
||||
from typing import TypedDict, Final, Literal, Any
|
||||
|
||||
Person = TypedDict("Person", {"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
|
||||
```
|
||||
|
||||
### Writing
|
||||
|
||||
`class.py`:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypedDict, Final, Literal, LiteralString, Any
|
||||
|
||||
@@ -470,10 +681,60 @@ def _(person: Person, unknown_key: Any):
|
||||
person[unknown_key] = "Eve"
|
||||
```
|
||||
|
||||
`functional.py`:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypedDict, Final, Literal, LiteralString, Any
|
||||
|
||||
Person = TypedDict("Person", {"name": str, "surname": str, "age": int | None})
|
||||
|
||||
NAME_FINAL: Final = "name"
|
||||
AGE_FINAL: Final[Literal["age"]] = "age"
|
||||
|
||||
def _(person: Person):
|
||||
person["name"] = "Alice"
|
||||
person["age"] = 30
|
||||
|
||||
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "naem" - did you mean "name"?"
|
||||
person["naem"] = "Alice"
|
||||
|
||||
def _(person: Person):
|
||||
person[NAME_FINAL] = "Alice"
|
||||
person[AGE_FINAL] = 30
|
||||
|
||||
def _(person: Person, literal_key: Literal["age"]):
|
||||
person[literal_key] = 22
|
||||
|
||||
def _(person: Person, union_of_keys: Literal["name", "surname"]):
|
||||
person[union_of_keys] = "unknown"
|
||||
|
||||
# error: [invalid-assignment] "Cannot assign value of type `Literal[1]` to key of type `Literal["name", "surname"]` on TypedDict `Person`"
|
||||
person[union_of_keys] = 1
|
||||
|
||||
def _(person: Person, union_of_keys: Literal["name", "age"], unknown_value: Any):
|
||||
person[union_of_keys] = unknown_value
|
||||
|
||||
# error: [invalid-assignment] "Cannot assign value of type `None` to key of type `Literal["name", "age"]` on TypedDict `Person`"
|
||||
person[union_of_keys] = None
|
||||
|
||||
def _(person: Person, str_key: str, literalstr_key: LiteralString):
|
||||
# error: [invalid-key] "Cannot access `Person` with a key of type `str`. Only string literals are allowed as keys on TypedDicts."
|
||||
person[str_key] = None
|
||||
|
||||
# error: [invalid-key] "Cannot access `Person` with a key of type `LiteralString`. Only string literals are allowed as keys on TypedDicts."
|
||||
person[literalstr_key] = None
|
||||
|
||||
def _(person: Person, unknown_key: Any):
|
||||
# No error here:
|
||||
person[unknown_key] = "Eve"
|
||||
```
|
||||
|
||||
## `ReadOnly`
|
||||
|
||||
Assignments to keys that are marked `ReadOnly` will produce an error:
|
||||
|
||||
`class.py`:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypedDict, ReadOnly, Required
|
||||
|
||||
@@ -489,10 +750,28 @@ alice["age"] = 31 # okay
|
||||
alice["id"] = 2
|
||||
```
|
||||
|
||||
`functional.py`:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypedDict, ReadOnly, Required
|
||||
|
||||
Person = TypedDict("Person", {"id": ReadOnly[Required[int]], "name": str, "age": int | None})
|
||||
|
||||
alice: Person = {"id": 1, "name": "Alice", "age": 30}
|
||||
alice["age"] = 31 # okay
|
||||
|
||||
# error: [invalid-assignment] "Cannot assign to key "id" on TypedDict `Person`: key is marked read-only"
|
||||
alice["id"] = 2
|
||||
```
|
||||
|
||||
This also works if all fields on a `TypedDict` are `ReadOnly`, in which case we synthesize a
|
||||
`__setitem__` method with a `key` type of `Never`:
|
||||
|
||||
`never_class.py`:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypedDict, ReadOnly, Required
|
||||
|
||||
class Config(TypedDict):
|
||||
host: ReadOnly[str]
|
||||
port: ReadOnly[int]
|
||||
@@ -505,8 +784,25 @@ config["host"] = "127.0.0.1"
|
||||
config["port"] = 80
|
||||
```
|
||||
|
||||
`never_functional.py`:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypedDict, ReadOnly, Required
|
||||
|
||||
Config = TypedDict("Config", {"host": ReadOnly[str], "port": ReadOnly[int]})
|
||||
|
||||
config: Config = {"host": "localhost", "port": 8080}
|
||||
|
||||
# error: [invalid-assignment] "Cannot assign to key "host" on TypedDict `Config`: key is marked read-only"
|
||||
config["host"] = "127.0.0.1"
|
||||
# error: [invalid-assignment] "Cannot assign to key "port" on TypedDict `Config`: key is marked read-only"
|
||||
config["port"] = 80
|
||||
```
|
||||
|
||||
## Methods on `TypedDict`
|
||||
|
||||
`class.py`
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
from typing_extensions import NotRequired
|
||||
@@ -556,6 +852,54 @@ def _(p: Person) -> None:
|
||||
reveal_type(p.setdefault("extraz", "value")) # revealed: Unknown
|
||||
```
|
||||
|
||||
`functional.py`:
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
Person = TypedDict("Person", {"name": str, "age": int | None, "extra": NotRequired[str]})
|
||||
|
||||
def _(p: Person) -> None:
|
||||
reveal_type(p.keys()) # revealed: dict_keys[str, object]
|
||||
reveal_type(p.values()) # revealed: dict_values[str, object]
|
||||
|
||||
# `get()` returns the field type for required keys (no None union)
|
||||
reveal_type(p.get("name")) # revealed: str
|
||||
reveal_type(p.get("age")) # revealed: int | None
|
||||
|
||||
# It doesn't matter if a default is specified:
|
||||
reveal_type(p.get("name", "default")) # revealed: str
|
||||
reveal_type(p.get("age", 999)) # revealed: int | None
|
||||
|
||||
# `get()` can return `None` for non-required keys
|
||||
reveal_type(p.get("extra")) # revealed: str | None
|
||||
reveal_type(p.get("extra", "default")) # revealed: str
|
||||
|
||||
# The type of the default parameter can be anything:
|
||||
reveal_type(p.get("extra", 0)) # revealed: str | Literal[0]
|
||||
|
||||
# We allow access to unknown keys (they could be set for a subtype of Person)
|
||||
reveal_type(p.get("unknown")) # revealed: Unknown | None
|
||||
reveal_type(p.get("unknown", "default")) # revealed: Unknown | Literal["default"]
|
||||
|
||||
# `pop()` only works on non-required fields
|
||||
reveal_type(p.pop("extra")) # revealed: str
|
||||
reveal_type(p.pop("extra", "fallback")) # revealed: str
|
||||
# error: [invalid-argument-type] "Cannot pop required field 'name' from TypedDict `Person`"
|
||||
reveal_type(p.pop("name")) # revealed: Unknown
|
||||
|
||||
# Similar to above, the default parameter can be of any type:
|
||||
reveal_type(p.pop("extra", 0)) # revealed: str | Literal[0]
|
||||
|
||||
# `setdefault()` always returns the field type
|
||||
reveal_type(p.setdefault("name", "Alice")) # revealed: str
|
||||
reveal_type(p.setdefault("extra", "default")) # revealed: str
|
||||
|
||||
# error: [invalid-key] "Invalid key access on TypedDict `Person`: Unknown key "extraz" - did you mean "extra"?"
|
||||
reveal_type(p.setdefault("extraz", "value")) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Unlike normal classes
|
||||
|
||||
`TypedDict` types do not act like normal classes. For example, calling `type(..)` on an inhabitant
|
||||
@@ -638,6 +982,26 @@ def accepts_typed_dict_class(t_person: type[Person]) -> None:
|
||||
accepts_typed_dict_class(Person)
|
||||
```
|
||||
|
||||
Similarly for `TypedDict`s created with the functional syntax:
|
||||
|
||||
```py
|
||||
Person2 = TypedDict("Person2", {"name": str, "age": int | None})
|
||||
|
||||
reveal_type(Person2.__total__) # revealed: bool
|
||||
reveal_type(Person2.__required_keys__) # revealed: frozenset[str]
|
||||
reveal_type(Person2.__optional_keys__) # revealed: frozenset[str]
|
||||
|
||||
def _(person: Person2) -> None:
|
||||
person.__total__ # error: [unresolved-attribute]
|
||||
person.__required_keys__ # error: [unresolved-attribute]
|
||||
person.__optional_keys__ # error: [unresolved-attribute]
|
||||
|
||||
def _(person: Person2) -> None:
|
||||
type(person).__total__ # error: [unresolved-attribute]
|
||||
type(person).__required_keys__ # error: [unresolved-attribute]
|
||||
type(person).__optional_keys__ # error: [unresolved-attribute]
|
||||
```
|
||||
|
||||
## Subclassing
|
||||
|
||||
`TypedDict` types can be subclassed. The subclass can add new keys:
|
||||
@@ -736,6 +1100,20 @@ emp_invalid1 = Employee(department="HR")
|
||||
emp_invalid2 = Employee(id=3)
|
||||
```
|
||||
|
||||
Fields from functional `TypedDict`s are not currently inherited:
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
|
||||
class X(TypedDict("Y", {"y": int})):
|
||||
x: int
|
||||
|
||||
x: X = {"x": 0}
|
||||
|
||||
# error: [invalid-key] "Invalid key access on TypedDict `X`: Unknown key "y""
|
||||
x: X = {"y": 0, "x": 0}
|
||||
```
|
||||
|
||||
## Generic `TypedDict`
|
||||
|
||||
`TypedDict`s can also be generic.
|
||||
@@ -801,27 +1179,6 @@ nested: Node = {"name": "n1", "parent": {"name": "n2", "parent": {"name": "n3",
|
||||
nested_invalid: Node = {"name": "n1", "parent": {"name": "n2", "parent": {"name": 3, "parent": None}}}
|
||||
```
|
||||
|
||||
## Function/assignment syntax
|
||||
|
||||
This is not yet supported. Make sure that we do not emit false positives for this syntax:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypedDict, Required
|
||||
|
||||
# Alternative syntax
|
||||
Message = TypedDict("Message", {"id": Required[int], "content": str}, total=False)
|
||||
|
||||
msg = Message(id=1, content="Hello")
|
||||
|
||||
# No errors for yet-unsupported features (`closed`):
|
||||
OtherMessage = TypedDict("OtherMessage", {"id": int, "content": str}, closed=True)
|
||||
|
||||
reveal_type(Message.__required_keys__) # revealed: @Todo(Support for `TypedDict`)
|
||||
|
||||
# TODO: this should be an error
|
||||
msg.content
|
||||
```
|
||||
|
||||
## Error cases
|
||||
|
||||
### `typing.TypedDict` is not allowed in type expressions
|
||||
|
||||
@@ -66,6 +66,7 @@ use crate::types::mro::{Mro, MroError, MroIterator};
|
||||
pub(crate) use crate::types::narrow::infer_narrowing_constraint;
|
||||
use crate::types::signatures::{ParameterForm, walk_signature};
|
||||
use crate::types::tuple::TupleSpec;
|
||||
use crate::types::typed_dict::{FunctionalTypedDictType, TypedDictSchema};
|
||||
pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type};
|
||||
use crate::types::variance::{TypeVarVariance, VarianceInferable};
|
||||
use crate::types::visitor::any_over_type;
|
||||
@@ -1009,6 +1010,19 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Turn a typed dict class literal or functional type dict type into a `TypedDictType`.
|
||||
pub(crate) fn to_typed_dict_type(self, db: &'db dyn Db) -> Option<TypedDictType<'db>> {
|
||||
match self {
|
||||
Type::ClassLiteral(class_literal) if class_literal.is_typed_dict(db) => Some(
|
||||
TypedDictType::from_class(ClassType::NonGeneric(class_literal)),
|
||||
),
|
||||
Type::KnownInstance(KnownInstanceType::TypedDictType(typed_dict)) => {
|
||||
Some(TypedDictType::Functional(typed_dict))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Turn a class literal (`Type::ClassLiteral` or `Type::GenericAlias`) into a `ClassType`.
|
||||
/// Since a `ClassType` must be specialized, apply the default specialization to any
|
||||
/// unspecialized generic class literal.
|
||||
@@ -1127,7 +1141,7 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
|
||||
pub(crate) fn typed_dict(defining_class: impl Into<ClassType<'db>>) -> Self {
|
||||
Self::TypedDict(TypedDictType::new(defining_class.into()))
|
||||
Self::TypedDict(TypedDictType::from_class(defining_class.into()))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@@ -1253,10 +1267,9 @@ impl<'db> Type<'db> {
|
||||
// Always normalize single-member enums to their class instance (`Literal[Single.VALUE]` => `Single`)
|
||||
enum_literal.enum_class_instance(db)
|
||||
}
|
||||
Type::TypedDict(_) => {
|
||||
// TODO: Normalize TypedDicts
|
||||
self
|
||||
}
|
||||
Type::TypedDict(typed_dict) => visitor.visit(self, || {
|
||||
Type::TypedDict(typed_dict.normalized_impl(db, visitor))
|
||||
}),
|
||||
Type::TypeAlias(alias) => alias.value_type(db).normalized_impl(db, visitor),
|
||||
Type::LiteralString
|
||||
| Type::AlwaysFalsy
|
||||
@@ -1510,6 +1523,11 @@ impl<'db> Type<'db> {
|
||||
.has_relation_to_impl(db, right, relation, visitor)
|
||||
}
|
||||
|
||||
(
|
||||
Type::KnownInstance(KnownInstanceType::TypedDictSchema(_)),
|
||||
Type::SpecialForm(SpecialFormType::TypedDictSchema),
|
||||
) => ConstraintSet::from(true),
|
||||
|
||||
// Dynamic is only a subtype of `object` and only a supertype of `Never`; both were
|
||||
// handled above. It's always assignable, though.
|
||||
//
|
||||
@@ -2974,6 +2992,14 @@ impl<'db> Type<'db> {
|
||||
policy: MemberLookupPolicy,
|
||||
) -> PlaceAndQualifiers<'db> {
|
||||
tracing::trace!("class_member: {}.{}", self.display(db), name);
|
||||
|
||||
if let Type::TypedDict(TypedDictType::Functional(typed_dict)) = self
|
||||
&& let Some(member) =
|
||||
TypedDictType::synthesized_member(db, self, typed_dict.items(db), &name)
|
||||
{
|
||||
return Place::bound(member).into();
|
||||
}
|
||||
|
||||
match self {
|
||||
Type::Union(union) => union.map_with_boundness_and_qualifiers(db, |elem| {
|
||||
elem.class_member_with_policy(db, name.clone(), policy)
|
||||
@@ -3728,7 +3754,7 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
|
||||
Type::ClassLiteral(..) | Type::GenericAlias(..) | Type::SubclassOf(..) => {
|
||||
let class_attr_plain = self.find_name_in_mro_with_policy(db, name_str,policy).expect(
|
||||
let class_attr_plain = self.find_name_in_mro_with_policy(db, name_str, policy).expect(
|
||||
"Calling `find_name_in_mro` on class literals and subclass-of types should always return `Some`",
|
||||
);
|
||||
|
||||
@@ -4746,7 +4772,9 @@ impl<'db> Type<'db> {
|
||||
Parameter::positional_only(Some(Name::new_static("typename")))
|
||||
.with_annotated_type(KnownClass::Str.to_instance(db)),
|
||||
Parameter::positional_only(Some(Name::new_static("fields")))
|
||||
.with_annotated_type(KnownClass::Dict.to_instance(db))
|
||||
.with_annotated_type(Type::SpecialForm(
|
||||
SpecialFormType::TypedDictSchema,
|
||||
))
|
||||
.with_default_type(Type::any()),
|
||||
Parameter::keyword_only(Name::new_static("total"))
|
||||
.with_annotated_type(KnownClass::Bool.to_instance(db))
|
||||
@@ -4765,6 +4793,8 @@ impl<'db> Type<'db> {
|
||||
Binding::single(self, Signature::todo("functional `NamedTuple` syntax")).into()
|
||||
}
|
||||
|
||||
Type::SpecialForm(_) => CallableBinding::not_callable(self).into(),
|
||||
|
||||
Type::GenericAlias(_) => {
|
||||
// TODO annotated return type on `__new__` or metaclass `__call__`
|
||||
// TODO check call vs signatures of `__new__` and/or `__init__`
|
||||
@@ -4833,11 +4863,24 @@ impl<'db> Type<'db> {
|
||||
// TODO: this is actually callable
|
||||
Type::DataclassDecorator(_) => CallableBinding::not_callable(self).into(),
|
||||
|
||||
// TODO: some `SpecialForm`s are callable (e.g. TypedDicts)
|
||||
Type::SpecialForm(_) => CallableBinding::not_callable(self).into(),
|
||||
|
||||
Type::EnumLiteral(enum_literal) => enum_literal.enum_class_instance(db).bindings(db),
|
||||
|
||||
Type::KnownInstance(KnownInstanceType::TypedDictType(typed_dict)) => {
|
||||
CallableBinding::from_overloads(
|
||||
self,
|
||||
[Signature::new(
|
||||
// TODO: List more specific parameter types here for better code completion.
|
||||
Parameters::new([
|
||||
Parameter::variadic(Name::new_static("args")),
|
||||
Parameter::keyword_variadic(Name::new_static("kwargs"))
|
||||
.with_annotated_type(Type::any()),
|
||||
]),
|
||||
Some(Type::TypedDict(TypedDictType::Functional(typed_dict))),
|
||||
)],
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
Type::KnownInstance(known_instance) => {
|
||||
known_instance.instance_fallback(db).bindings(db)
|
||||
}
|
||||
@@ -5641,6 +5684,15 @@ impl<'db> Type<'db> {
|
||||
.map(Type::NonInferableTypeVar)
|
||||
.unwrap_or(*self))
|
||||
}
|
||||
KnownInstanceType::TypedDictType(typed_dict) => {
|
||||
Ok(Type::TypedDict(TypedDictType::Functional(*typed_dict)))
|
||||
}
|
||||
KnownInstanceType::TypedDictSchema(_) => Err(InvalidTypeExpressionError {
|
||||
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType(
|
||||
*self, scope_id
|
||||
)],
|
||||
fallback_type: Type::unknown(),
|
||||
}),
|
||||
KnownInstanceType::Deprecated(_) => Err(InvalidTypeExpressionError {
|
||||
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Deprecated],
|
||||
fallback_type: Type::unknown(),
|
||||
@@ -5731,6 +5783,12 @@ impl<'db> Type<'db> {
|
||||
],
|
||||
fallback_type: Type::unknown(),
|
||||
}),
|
||||
SpecialFormType::TypedDictSchema => Err(InvalidTypeExpressionError {
|
||||
invalid_expressions: smallvec::smallvec_inline![
|
||||
InvalidTypeExpression::InvalidType(*self, scope_id)
|
||||
],
|
||||
fallback_type: Type::unknown(),
|
||||
}),
|
||||
|
||||
SpecialFormType::Literal
|
||||
| SpecialFormType::Union
|
||||
@@ -5927,9 +5985,7 @@ impl<'db> Type<'db> {
|
||||
Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db),
|
||||
Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db),
|
||||
Type::ProtocolInstance(protocol) => protocol.to_meta_type(db),
|
||||
// `TypedDict` instances are instances of `dict` at runtime, but its important that we
|
||||
// understand a more specific meta type in order to correctly handle `__getitem__`.
|
||||
Type::TypedDict(typed_dict) => SubclassOfType::from(db, typed_dict.defining_class()),
|
||||
Type::TypedDict(typed_dict) => typed_dict.to_meta_type(db),
|
||||
Type::TypeAlias(alias) => alias.value_type(db).to_meta_type(db),
|
||||
}
|
||||
}
|
||||
@@ -6486,8 +6542,10 @@ impl<'db> Type<'db> {
|
||||
Protocol::Synthesized(_) => None,
|
||||
},
|
||||
|
||||
Type::TypedDict(typed_dict) => {
|
||||
Some(TypeDefinition::Class(typed_dict.defining_class().definition(db)))
|
||||
Type::TypedDict(typed_dict) => match typed_dict {
|
||||
TypedDictType::ClassBased(class) => Some(TypeDefinition::Class(class.definition(db))),
|
||||
// TODO: Support go-to-definition for functional `TypedDict`s.
|
||||
TypedDictType::Functional(_) => None,
|
||||
}
|
||||
|
||||
Self::Union(_) | Self::Intersection(_) => None,
|
||||
@@ -6836,6 +6894,13 @@ pub enum KnownInstanceType<'db> {
|
||||
/// A single instance of `typing.TypeAliasType` (PEP 695 type alias)
|
||||
TypeAliasType(TypeAliasType<'db>),
|
||||
|
||||
/// A single class object created using the `typing.TypedDict` functional syntax
|
||||
TypedDictType(FunctionalTypedDictType<'db>),
|
||||
|
||||
/// An internal type representing the dictionary literal argument to the functional `TypedDict`
|
||||
/// constructor.
|
||||
TypedDictSchema(TypedDictSchema<'db>),
|
||||
|
||||
/// A single instance of `warnings.deprecated` or `typing_extensions.deprecated`
|
||||
Deprecated(DeprecatedInstance<'db>),
|
||||
|
||||
@@ -6863,7 +6928,12 @@ fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
|
||||
KnownInstanceType::TypeAliasType(type_alias) => {
|
||||
visitor.visit_type_alias_type(db, type_alias);
|
||||
}
|
||||
KnownInstanceType::Deprecated(_) | KnownInstanceType::ConstraintSet(_) => {
|
||||
KnownInstanceType::TypedDictType(typed_dict) => {
|
||||
visitor.visit_typed_dict_type(db, TypedDictType::Functional(typed_dict));
|
||||
}
|
||||
KnownInstanceType::Deprecated(_)
|
||||
| KnownInstanceType::ConstraintSet(_)
|
||||
| KnownInstanceType::TypedDictSchema(_) => {
|
||||
// Nothing to visit
|
||||
}
|
||||
KnownInstanceType::Field(field) => {
|
||||
@@ -6885,15 +6955,11 @@ impl<'db> KnownInstanceType<'db> {
|
||||
Self::TypeAliasType(type_alias) => {
|
||||
Self::TypeAliasType(type_alias.normalized_impl(db, visitor))
|
||||
}
|
||||
Self::Deprecated(deprecated) => {
|
||||
// Nothing to normalize
|
||||
Self::Deprecated(deprecated)
|
||||
Self::TypedDictType(typed_dict) => {
|
||||
Self::TypedDictType(typed_dict.normalized_impl(db, visitor))
|
||||
}
|
||||
Self::Field(field) => Self::Field(field.normalized_impl(db, visitor)),
|
||||
Self::ConstraintSet(set) => {
|
||||
// Nothing to normalize
|
||||
Self::ConstraintSet(set)
|
||||
}
|
||||
Self::Deprecated(_) | Self::TypedDictSchema(_) | Self::ConstraintSet(_) => self,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6905,6 +6971,8 @@ impl<'db> KnownInstanceType<'db> {
|
||||
KnownClass::GenericAlias
|
||||
}
|
||||
Self::TypeAliasType(_) => KnownClass::TypeAliasType,
|
||||
Self::TypedDictType(_) => KnownClass::TypedDictFallback,
|
||||
Self::TypedDictSchema(_) => KnownClass::Object,
|
||||
Self::Deprecated(_) => KnownClass::Deprecated,
|
||||
Self::Field(_) => KnownClass::Field,
|
||||
Self::ConstraintSet(_) => KnownClass::ConstraintSet,
|
||||
@@ -6961,6 +7029,8 @@ impl<'db> KnownInstanceType<'db> {
|
||||
f.write_str("typing.TypeAliasType")
|
||||
}
|
||||
}
|
||||
KnownInstanceType::TypedDictType(_) => f.write_str("typing.TypedDict"),
|
||||
KnownInstanceType::TypedDictSchema(_) => f.write_str("_TypedDictSchema"),
|
||||
// This is a legacy `TypeVar` _outside_ of any generic class or function, so we render
|
||||
// it as an instance of `typing.TypeVar`. Inside of a generic class or function, we'll
|
||||
// have a `Type::TypeVar(_)`, which is rendered as the typevar's name.
|
||||
@@ -9119,7 +9189,7 @@ impl TypeRelation {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, get_size2::GetSize)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
|
||||
pub enum Truthiness {
|
||||
/// For an object `x`, `bool(x)` will always return `True`
|
||||
AlwaysTrue,
|
||||
@@ -9166,6 +9236,14 @@ impl Truthiness {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn unwrap_or(self, value: bool) -> bool {
|
||||
match self {
|
||||
Truthiness::AlwaysTrue => true,
|
||||
Truthiness::AlwaysFalse => false,
|
||||
Truthiness::Ambiguous => value,
|
||||
}
|
||||
}
|
||||
|
||||
fn into_type(self, db: &dyn Db) -> Type<'_> {
|
||||
match self {
|
||||
Self::AlwaysTrue => Type::BooleanLiteral(true),
|
||||
|
||||
@@ -12,11 +12,11 @@ use ruff_python_ast::name::Name;
|
||||
use smallvec::{SmallVec, smallvec, smallvec_inline};
|
||||
|
||||
use super::{Argument, CallArguments, CallError, CallErrorKind, InferContext, Signature, Type};
|
||||
use crate::Program;
|
||||
use crate::db::Db;
|
||||
use crate::dunder_all::dunder_all_names;
|
||||
use crate::place::{Boundness, Place};
|
||||
use crate::types::call::arguments::{Expansion, is_expandable_type};
|
||||
use crate::types::class::{Field, FieldKind};
|
||||
use crate::types::diagnostic::{
|
||||
CALL_NON_CALLABLE, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, MISSING_ARGUMENT,
|
||||
NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, POSITIONAL_ONLY_PARAMETER_AS_KWARG,
|
||||
@@ -29,12 +29,14 @@ use crate::types::function::{
|
||||
use crate::types::generics::{Specialization, SpecializationBuilder, SpecializationError};
|
||||
use crate::types::signatures::{Parameter, ParameterForm, ParameterKind, Parameters};
|
||||
use crate::types::tuple::{TupleLength, TupleType};
|
||||
use crate::types::typed_dict::FunctionalTypedDictType;
|
||||
use crate::types::{
|
||||
BoundMethodType, ClassLiteral, DataclassParams, FieldInstance, KnownBoundMethodType,
|
||||
KnownClass, KnownInstanceType, MemberLookupPolicy, PropertyInstanceType, SpecialFormType,
|
||||
TrackedConstraintSet, TypeAliasType, TypeContext, UnionBuilder, UnionType,
|
||||
WrapperDescriptorKind, enums, ide_support, infer_isolated_expression, todo_type,
|
||||
TrackedConstraintSet, TypeAliasType, TypeContext, TypedDictParams, UnionBuilder, UnionType,
|
||||
WrapperDescriptorKind, enums, ide_support, infer_isolated_expression,
|
||||
};
|
||||
use crate::{FxOrderMap, Program};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
|
||||
use ruff_python_ast::{self as ast, ArgOrKeyword, PythonVersion};
|
||||
|
||||
@@ -1096,7 +1098,52 @@ impl<'db> Bindings<'db> {
|
||||
},
|
||||
|
||||
Type::SpecialForm(SpecialFormType::TypedDict) => {
|
||||
overload.set_return_type(todo_type!("Support for `TypedDict`"));
|
||||
let [
|
||||
Some(name),
|
||||
Some(Type::KnownInstance(KnownInstanceType::TypedDictSchema(schema))),
|
||||
total,
|
||||
..,
|
||||
] = overload.parameter_types()
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Some(name) = name.into_string_literal() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut params = TypedDictParams::empty();
|
||||
|
||||
let is_total = to_bool(total, true);
|
||||
params.set(TypedDictParams::TOTAL, is_total);
|
||||
|
||||
let items = schema
|
||||
.items(db)
|
||||
.iter()
|
||||
.map(|(name, field)| {
|
||||
let field = Field {
|
||||
single_declaration: None,
|
||||
declared_ty: field.declared_ty,
|
||||
kind: FieldKind::TypedDict {
|
||||
is_read_only: field.is_read_only,
|
||||
// If there is no explicit `Required` or `NotRequired` qualifier, use
|
||||
// the `total` parameter.
|
||||
is_required: field.is_required.unwrap_or(is_total),
|
||||
},
|
||||
};
|
||||
|
||||
(name.clone(), field)
|
||||
})
|
||||
.collect::<FxOrderMap<_, _>>();
|
||||
|
||||
overload.set_return_type(Type::KnownInstance(
|
||||
KnownInstanceType::TypedDictType(FunctionalTypedDictType::new(
|
||||
db,
|
||||
Name::new(name.value(db)),
|
||||
params,
|
||||
items,
|
||||
)),
|
||||
));
|
||||
}
|
||||
|
||||
// Not a special case
|
||||
@@ -2346,7 +2393,7 @@ impl<'a, 'db> ArgumentMatcher<'a, 'db> {
|
||||
) {
|
||||
if let Some(Type::TypedDict(typed_dict)) = argument_type {
|
||||
// Special case TypedDict because we know which keys are present.
|
||||
for (name, field) in typed_dict.items(db) {
|
||||
for (name, field) in typed_dict.items(db).as_ref() {
|
||||
let _ = self.match_keyword(
|
||||
argument_index,
|
||||
Argument::Keywords,
|
||||
|
||||
@@ -28,9 +28,9 @@ use crate::types::{
|
||||
ApplyTypeMappingVisitor, Binding, BoundSuperError, BoundSuperType, CallableType,
|
||||
DataclassParams, DeprecatedInstance, FindLegacyTypeVarsVisitor, HasRelationToVisitor,
|
||||
IsEquivalentVisitor, KnownInstanceType, ManualPEP695TypeAliasType, MaterializationKind,
|
||||
NormalizedVisitor, PropertyInstanceType, StringLiteralType, TypeAliasType, TypeContext,
|
||||
TypeMapping, TypeRelation, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind,
|
||||
TypedDictParams, UnionBuilder, VarianceInferable, declaration_type, determine_upper_bound,
|
||||
NormalizedVisitor, PropertyInstanceType, TypeAliasType, TypeContext, TypeMapping, TypeRelation,
|
||||
TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, TypedDictParams, TypedDictType,
|
||||
UnionBuilder, VarianceInferable, declaration_type, determine_upper_bound,
|
||||
infer_definition_types,
|
||||
};
|
||||
use crate::{
|
||||
@@ -1267,7 +1267,7 @@ impl MethodDecorator {
|
||||
}
|
||||
|
||||
/// Kind-specific metadata for different types of fields
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
|
||||
pub(crate) enum FieldKind<'db> {
|
||||
/// `NamedTuple` field metadata
|
||||
NamedTuple { default_ty: Option<Type<'db>> },
|
||||
@@ -1293,8 +1293,8 @@ pub(crate) enum FieldKind<'db> {
|
||||
}
|
||||
|
||||
/// Metadata regarding a dataclass field/attribute or a `TypedDict` "item" / key-value pair.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct Field<'db> {
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
|
||||
pub struct Field<'db> {
|
||||
/// The declared type of the field
|
||||
pub(crate) declared_ty: Type<'db>,
|
||||
/// Kind-specific metadata for this field
|
||||
@@ -1304,7 +1304,7 @@ pub(crate) struct Field<'db> {
|
||||
pub(crate) single_declaration: Option<Definition<'db>>,
|
||||
}
|
||||
|
||||
impl Field<'_> {
|
||||
impl<'db> Field<'db> {
|
||||
pub(crate) const fn is_required(&self) -> bool {
|
||||
match &self.kind {
|
||||
FieldKind::NamedTuple { default_ty } => default_ty.is_none(),
|
||||
@@ -1323,6 +1323,21 @@ impl Field<'_> {
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn apply_type_mapping_impl<'a>(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
type_mapping: &TypeMapping<'a, 'db>,
|
||||
visitor: &ApplyTypeMappingVisitor<'db>,
|
||||
) -> Self {
|
||||
Field {
|
||||
kind: self.kind,
|
||||
single_declaration: self.single_declaration,
|
||||
declared_ty: self
|
||||
.declared_ty
|
||||
.apply_type_mapping_impl(db, type_mapping, visitor),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> Field<'db> {
|
||||
@@ -2360,273 +2375,12 @@ impl<'db> ClassLiteral<'db> {
|
||||
Type::heterogeneous_tuple(db, slots)
|
||||
})
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "__setitem__") => {
|
||||
|
||||
(CodeGeneratorKind::TypedDict, _) => {
|
||||
let fields = self.fields(db, specialization, field_policy);
|
||||
|
||||
// Add (key type, value type) overloads for all TypedDict items ("fields") that are not read-only:
|
||||
|
||||
let mut writeable_fields = fields
|
||||
.iter()
|
||||
.filter(|(_, field)| !field.is_read_only())
|
||||
.peekable();
|
||||
|
||||
if writeable_fields.peek().is_none() {
|
||||
// If there are no writeable fields, synthesize a `__setitem__` that takes
|
||||
// a `key` of type `Never` to signal that no keys are accepted. This leads
|
||||
// to slightly more user-friendly error messages compared to returning an
|
||||
// empty overload set.
|
||||
return Some(Type::Callable(CallableType::new(
|
||||
db,
|
||||
CallableSignature::single(Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(Type::Never),
|
||||
Parameter::positional_only(Some(Name::new_static("value")))
|
||||
.with_annotated_type(Type::any()),
|
||||
]),
|
||||
Some(Type::none(db)),
|
||||
)),
|
||||
true,
|
||||
)));
|
||||
}
|
||||
|
||||
let overloads = writeable_fields.map(|(name, field)| {
|
||||
let key_type = Type::StringLiteral(StringLiteralType::new(db, name.as_str()));
|
||||
|
||||
Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(key_type),
|
||||
Parameter::positional_only(Some(Name::new_static("value")))
|
||||
.with_annotated_type(field.declared_ty),
|
||||
]),
|
||||
Some(Type::none(db)),
|
||||
)
|
||||
});
|
||||
|
||||
Some(Type::Callable(CallableType::new(
|
||||
db,
|
||||
CallableSignature::from_overloads(overloads),
|
||||
true,
|
||||
)))
|
||||
TypedDictType::synthesized_member(db, instance_ty, &fields, name)
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "__getitem__") => {
|
||||
let fields = self.fields(db, specialization, field_policy);
|
||||
|
||||
// Add (key -> value type) overloads for all TypedDict items ("fields"):
|
||||
let overloads = fields.iter().map(|(name, field)| {
|
||||
let key_type = Type::StringLiteral(StringLiteralType::new(db, name.as_str()));
|
||||
|
||||
Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(key_type),
|
||||
]),
|
||||
Some(field.declared_ty),
|
||||
)
|
||||
});
|
||||
|
||||
Some(Type::Callable(CallableType::new(
|
||||
db,
|
||||
CallableSignature::from_overloads(overloads),
|
||||
true,
|
||||
)))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "get") => {
|
||||
let overloads = self
|
||||
.fields(db, specialization, field_policy)
|
||||
.into_iter()
|
||||
.flat_map(|(name, field)| {
|
||||
let key_type =
|
||||
Type::StringLiteral(StringLiteralType::new(db, name.as_str()));
|
||||
|
||||
// For a required key, `.get()` always returns the value type. For a non-required key,
|
||||
// `.get()` returns the union of the value type and the type of the default argument
|
||||
// (which defaults to `None`).
|
||||
|
||||
// TODO: For now, we use two overloads here. They can be merged into a single function
|
||||
// once the generics solver takes default arguments into account.
|
||||
|
||||
let get_sig = Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(key_type),
|
||||
]),
|
||||
Some(if field.is_required() {
|
||||
field.declared_ty
|
||||
} else {
|
||||
UnionType::from_elements(db, [field.declared_ty, Type::none(db)])
|
||||
}),
|
||||
);
|
||||
|
||||
let t_default =
|
||||
BoundTypeVarInstance::synthetic(db, "T", TypeVarVariance::Covariant);
|
||||
|
||||
let get_with_default_sig = Signature::new_generic(
|
||||
Some(GenericContext::from_typevar_instances(db, [t_default])),
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(key_type),
|
||||
Parameter::positional_only(Some(Name::new_static("default")))
|
||||
.with_annotated_type(Type::TypeVar(t_default)),
|
||||
]),
|
||||
Some(if field.is_required() {
|
||||
field.declared_ty
|
||||
} else {
|
||||
UnionType::from_elements(
|
||||
db,
|
||||
[field.declared_ty, Type::TypeVar(t_default)],
|
||||
)
|
||||
}),
|
||||
);
|
||||
|
||||
[get_sig, get_with_default_sig]
|
||||
})
|
||||
// Fallback overloads for unknown keys
|
||||
.chain(std::iter::once({
|
||||
Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(KnownClass::Str.to_instance(db)),
|
||||
]),
|
||||
Some(UnionType::from_elements(
|
||||
db,
|
||||
[Type::unknown(), Type::none(db)],
|
||||
)),
|
||||
)
|
||||
}))
|
||||
.chain(std::iter::once({
|
||||
let t_default =
|
||||
BoundTypeVarInstance::synthetic(db, "T", TypeVarVariance::Covariant);
|
||||
|
||||
Signature::new_generic(
|
||||
Some(GenericContext::from_typevar_instances(db, [t_default])),
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(KnownClass::Str.to_instance(db)),
|
||||
Parameter::positional_only(Some(Name::new_static("default")))
|
||||
.with_annotated_type(Type::TypeVar(t_default)),
|
||||
]),
|
||||
Some(UnionType::from_elements(
|
||||
db,
|
||||
[Type::unknown(), Type::TypeVar(t_default)],
|
||||
)),
|
||||
)
|
||||
}));
|
||||
|
||||
Some(Type::Callable(CallableType::new(
|
||||
db,
|
||||
CallableSignature::from_overloads(overloads),
|
||||
true,
|
||||
)))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "pop") => {
|
||||
let fields = self.fields(db, specialization, field_policy);
|
||||
let overloads = fields
|
||||
.iter()
|
||||
.filter(|(_, field)| {
|
||||
// Only synthesize `pop` for fields that are not required.
|
||||
!field.is_required()
|
||||
})
|
||||
.flat_map(|(name, field)| {
|
||||
let key_type =
|
||||
Type::StringLiteral(StringLiteralType::new(db, name.as_str()));
|
||||
|
||||
// TODO: Similar to above: consider merging these two overloads into one
|
||||
|
||||
// `.pop()` without default
|
||||
let pop_sig = Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(key_type),
|
||||
]),
|
||||
Some(field.declared_ty),
|
||||
);
|
||||
|
||||
// `.pop()` with a default value
|
||||
let t_default =
|
||||
BoundTypeVarInstance::synthetic(db, "T", TypeVarVariance::Covariant);
|
||||
|
||||
let pop_with_default_sig = Signature::new_generic(
|
||||
Some(GenericContext::from_typevar_instances(db, [t_default])),
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(key_type),
|
||||
Parameter::positional_only(Some(Name::new_static("default")))
|
||||
.with_annotated_type(Type::TypeVar(t_default)),
|
||||
]),
|
||||
Some(UnionType::from_elements(
|
||||
db,
|
||||
[field.declared_ty, Type::TypeVar(t_default)],
|
||||
)),
|
||||
);
|
||||
|
||||
[pop_sig, pop_with_default_sig]
|
||||
});
|
||||
|
||||
Some(Type::Callable(CallableType::new(
|
||||
db,
|
||||
CallableSignature::from_overloads(overloads),
|
||||
true,
|
||||
)))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "setdefault") => {
|
||||
let fields = self.fields(db, specialization, field_policy);
|
||||
let overloads = fields.iter().map(|(name, field)| {
|
||||
let key_type = Type::StringLiteral(StringLiteralType::new(db, name.as_str()));
|
||||
|
||||
// `setdefault` always returns the field type
|
||||
Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(key_type),
|
||||
Parameter::positional_only(Some(Name::new_static("default")))
|
||||
.with_annotated_type(field.declared_ty),
|
||||
]),
|
||||
Some(field.declared_ty),
|
||||
)
|
||||
});
|
||||
|
||||
Some(Type::Callable(CallableType::new(
|
||||
db,
|
||||
CallableSignature::from_overloads(overloads),
|
||||
true,
|
||||
)))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "update") => {
|
||||
// TODO: synthesize a set of overloads with precise types
|
||||
let signature = Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::variadic(Name::new_static("args")),
|
||||
Parameter::keyword_variadic(Name::new_static("kwargs")),
|
||||
]),
|
||||
Some(Type::none(db)),
|
||||
);
|
||||
|
||||
Some(CallableType::function_like(db, signature))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,9 +169,12 @@ impl<'db> ClassBase<'db> {
|
||||
KnownInstanceType::SubscriptedProtocol(_) => Some(Self::Protocol),
|
||||
KnownInstanceType::TypeAliasType(_)
|
||||
| KnownInstanceType::TypeVar(_)
|
||||
| KnownInstanceType::TypedDictSchema(_)
|
||||
| KnownInstanceType::Deprecated(_)
|
||||
| KnownInstanceType::Field(_)
|
||||
| KnownInstanceType::ConstraintSet(_) => None,
|
||||
// TODO: Inherit the fields of functional `TypedDict`s.
|
||||
KnownInstanceType::TypedDictType(_) => Some(Self::TypedDict),
|
||||
},
|
||||
|
||||
Type::SpecialForm(special_form) => match special_form {
|
||||
@@ -200,7 +203,8 @@ impl<'db> ClassBase<'db> {
|
||||
| SpecialFormType::TypeOf
|
||||
| SpecialFormType::CallableTypeOf
|
||||
| SpecialFormType::AlwaysTruthy
|
||||
| SpecialFormType::AlwaysFalsy => None,
|
||||
| SpecialFormType::AlwaysFalsy
|
||||
| SpecialFormType::TypedDictSchema => None,
|
||||
|
||||
SpecialFormType::Any => Some(Self::Dynamic(DynamicType::Any)),
|
||||
SpecialFormType::Unknown => Some(Self::unknown()),
|
||||
|
||||
@@ -23,7 +23,7 @@ use crate::types::visitor::TypeVisitor;
|
||||
use crate::types::{
|
||||
BoundTypeVarInstance, CallableType, IntersectionType, KnownBoundMethodType, KnownClass,
|
||||
MaterializationKind, Protocol, ProtocolInstanceType, StringLiteralType, SubclassOfInner, Type,
|
||||
UnionType, WrapperDescriptorKind, visitor,
|
||||
TypedDictType, UnionType, WrapperDescriptorKind, visitor,
|
||||
};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
|
||||
@@ -510,12 +510,16 @@ impl Display for DisplayRepresentation<'_> {
|
||||
}
|
||||
f.write_str("]")
|
||||
}
|
||||
Type::TypedDict(typed_dict) => typed_dict
|
||||
.defining_class()
|
||||
.class_literal(self.db)
|
||||
.0
|
||||
.display_with(self.db, self.settings.clone())
|
||||
.fmt(f),
|
||||
|
||||
Type::TypedDict(typed_dict) => match typed_dict {
|
||||
TypedDictType::ClassBased(class) => class
|
||||
.class_literal(self.db)
|
||||
.0
|
||||
.display_with(self.db, self.settings.clone())
|
||||
.fmt(f),
|
||||
TypedDictType::Functional(synthesized) => synthesized.name(self.db).fmt(f),
|
||||
},
|
||||
|
||||
Type::TypeAlias(alias) => f.write_str(alias.name(self.db)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,8 @@ use crate::types::signatures::Signature;
|
||||
use crate::types::subclass_of::SubclassOfInner;
|
||||
use crate::types::tuple::{Tuple, TupleLength, TupleSpec, TupleType};
|
||||
use crate::types::typed_dict::{
|
||||
TypedDictAssignmentKind, validate_typed_dict_constructor, validate_typed_dict_dict_literal,
|
||||
TypedDictAssignmentKind, TypedDictSchema, TypedDictSchemaField,
|
||||
validate_typed_dict_constructor, validate_typed_dict_dict_literal,
|
||||
validate_typed_dict_key_assignment,
|
||||
};
|
||||
use crate::types::visitor::any_over_type;
|
||||
@@ -95,13 +96,13 @@ use crate::types::{
|
||||
Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet, Truthiness, Type,
|
||||
TypeAliasType, TypeAndQualifiers, TypeContext, TypeQualifiers,
|
||||
TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarInstance, TypeVarKind,
|
||||
TypedDictType, UnionBuilder, UnionType, binding_type, todo_type,
|
||||
UnionBuilder, UnionType, binding_type, todo_type,
|
||||
};
|
||||
use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic};
|
||||
use crate::unpack::{EvaluationMode, UnpackPosition};
|
||||
use crate::util::diagnostics::format_enumeration;
|
||||
use crate::util::subscript::{PyIndex, PySlice};
|
||||
use crate::{Db, FxOrderSet, Program};
|
||||
use crate::{Db, FxOrderMap, FxOrderSet, Program};
|
||||
|
||||
mod annotation_expression;
|
||||
mod type_expression;
|
||||
@@ -5471,10 +5472,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
items,
|
||||
} = dict;
|
||||
|
||||
// Validate `TypedDict` dictionary literal assignments.
|
||||
if let Some(typed_dict) = tcx.annotation.and_then(Type::into_typed_dict)
|
||||
&& let Some(ty) = self.infer_typed_dict_expression(dict, typed_dict)
|
||||
{
|
||||
// Infer `TypedDict` dictionary literal assignments.
|
||||
if let Some(ty) = self.infer_typed_dict_expression(dict, tcx) {
|
||||
return ty;
|
||||
}
|
||||
|
||||
// Infer the dictionary literal passed to the functional `TypedDict` constructor.
|
||||
if let Some(ty) = self.infer_typed_dict_schema(dict, tcx) {
|
||||
return ty;
|
||||
}
|
||||
|
||||
@@ -5496,39 +5500,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
})
|
||||
}
|
||||
|
||||
fn infer_typed_dict_expression(
|
||||
&mut self,
|
||||
dict: &ast::ExprDict,
|
||||
typed_dict: TypedDictType<'db>,
|
||||
) -> Option<Type<'db>> {
|
||||
let ast::ExprDict {
|
||||
range: _,
|
||||
node_index: _,
|
||||
items,
|
||||
} = dict;
|
||||
|
||||
let typed_dict_items = typed_dict.items(self.db());
|
||||
|
||||
for item in items {
|
||||
self.infer_optional_expression(item.key.as_ref(), TypeContext::default());
|
||||
|
||||
if let Some(ast::Expr::StringLiteral(ref key)) = item.key
|
||||
&& let Some(key) = key.as_single_part_string()
|
||||
&& let Some(field) = typed_dict_items.get(key.as_str())
|
||||
{
|
||||
self.infer_expression(&item.value, TypeContext::new(Some(field.declared_ty)));
|
||||
} else {
|
||||
self.infer_expression(&item.value, TypeContext::default());
|
||||
}
|
||||
}
|
||||
|
||||
validate_typed_dict_dict_literal(&self.context, typed_dict, dict, dict.into(), |expr| {
|
||||
self.expression_type(expr)
|
||||
})
|
||||
.ok()
|
||||
.map(|_| Type::TypedDict(typed_dict))
|
||||
}
|
||||
|
||||
// Infer the type of a collection literal expression.
|
||||
fn infer_collection_literal<'expr, const N: usize>(
|
||||
&mut self,
|
||||
@@ -5625,6 +5596,96 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
Type::from(class_type).to_instance(self.db())
|
||||
}
|
||||
|
||||
fn infer_typed_dict_expression(
|
||||
&mut self,
|
||||
dict: &ast::ExprDict,
|
||||
tcx: TypeContext<'db>,
|
||||
) -> Option<Type<'db>> {
|
||||
let ast::ExprDict {
|
||||
range: _,
|
||||
node_index: _,
|
||||
items,
|
||||
} = dict;
|
||||
|
||||
let typed_dict = tcx.annotation.and_then(Type::into_typed_dict)?;
|
||||
let typed_dict_items = typed_dict.items(self.db());
|
||||
|
||||
for item in items {
|
||||
self.infer_optional_expression(item.key.as_ref(), TypeContext::default());
|
||||
|
||||
if let Some(ast::Expr::StringLiteral(ref key)) = item.key
|
||||
&& let Some(key) = key.as_single_part_string()
|
||||
&& let Some(field) = typed_dict_items.get(key.as_str())
|
||||
{
|
||||
self.infer_expression(&item.value, TypeContext::new(Some(field.declared_ty)));
|
||||
} else {
|
||||
self.infer_expression(&item.value, TypeContext::default());
|
||||
}
|
||||
}
|
||||
|
||||
validate_typed_dict_dict_literal(&self.context, typed_dict, dict, dict.into(), |expr| {
|
||||
self.expression_type(expr)
|
||||
})
|
||||
.ok()
|
||||
.map(|_| Type::TypedDict(typed_dict))
|
||||
}
|
||||
|
||||
// Infer the dictionary literal passed to the functional `TypedDict` constructor.
|
||||
fn infer_typed_dict_schema(
|
||||
&mut self,
|
||||
dict: &ast::ExprDict,
|
||||
tcx: TypeContext<'db>,
|
||||
) -> Option<Type<'db>> {
|
||||
let ast::ExprDict {
|
||||
range: _,
|
||||
node_index: _,
|
||||
items,
|
||||
} = dict;
|
||||
|
||||
let Some(Type::SpecialForm(SpecialFormType::TypedDictSchema)) = tcx.annotation else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut typed_dict_items = FxOrderMap::default();
|
||||
|
||||
for item in items {
|
||||
let Some(Type::StringLiteral(key)) =
|
||||
self.infer_optional_expression(item.key.as_ref(), TypeContext::default())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// TODO: The only qualifiers supported on `TypedDict` are `Required`, `NotRequired`,
|
||||
// and `ReadOnly`. We may want to error if others are used.
|
||||
let field_ty =
|
||||
self.infer_annotation_expression(&item.value, DeferredExpressionState::None);
|
||||
|
||||
let is_required = if field_ty.qualifiers.contains(TypeQualifiers::REQUIRED) {
|
||||
// Explicit Required[T] annotation - always required
|
||||
Truthiness::AlwaysTrue
|
||||
} else if field_ty.qualifiers.contains(TypeQualifiers::NOT_REQUIRED) {
|
||||
// Explicit NotRequired[T] annotation - never required
|
||||
Truthiness::AlwaysFalse
|
||||
} else {
|
||||
// No explicit qualifier - we don't have access to the `total` qualifier here,
|
||||
// so we leave this to be filled in by the `TypedDict` constructor.
|
||||
Truthiness::Ambiguous
|
||||
};
|
||||
|
||||
let field = TypedDictSchemaField {
|
||||
is_required,
|
||||
declared_ty: field_ty.inner_type(),
|
||||
is_read_only: field_ty.qualifiers.contains(TypeQualifiers::READ_ONLY),
|
||||
};
|
||||
|
||||
typed_dict_items.insert(ast::name::Name::new(key.value(self.db())), field);
|
||||
}
|
||||
|
||||
Some(Type::KnownInstance(KnownInstanceType::TypedDictSchema(
|
||||
TypedDictSchema::new(self.db(), typed_dict_items),
|
||||
)))
|
||||
}
|
||||
|
||||
/// Infer the type of the `iter` expression of the first comprehension.
|
||||
fn infer_first_comprehension_iter(&mut self, comprehensions: &[ast::Comprehension]) {
|
||||
let mut comprehensions_iter = comprehensions.iter();
|
||||
@@ -6013,7 +6074,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
ty
|
||||
});
|
||||
|
||||
// TODO: Use the type context for more precise inference.
|
||||
let callable_type = self.infer_maybe_standalone_expression(func, TypeContext::default());
|
||||
|
||||
// Special handling for `TypedDict` method calls
|
||||
@@ -6157,19 +6217,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
||||
self.infer_all_argument_types(arguments, &mut call_arguments, &bindings);
|
||||
|
||||
// Validate `TypedDict` constructor calls after argument type inference
|
||||
if let Some(class_literal) = callable_type.into_class_literal() {
|
||||
if class_literal.is_typed_dict(self.db()) {
|
||||
let typed_dict_type = Type::typed_dict(ClassType::NonGeneric(class_literal));
|
||||
if let Some(typed_dict) = typed_dict_type.into_typed_dict() {
|
||||
validate_typed_dict_constructor(
|
||||
&self.context,
|
||||
typed_dict,
|
||||
arguments,
|
||||
func.as_ref().into(),
|
||||
|expr| self.expression_type(expr),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(typed_dict) = callable_type.to_typed_dict_type(self.db()) {
|
||||
validate_typed_dict_constructor(
|
||||
&self.context,
|
||||
typed_dict,
|
||||
arguments,
|
||||
func.as_ref().into(),
|
||||
|expr| self.expression_type(expr),
|
||||
);
|
||||
}
|
||||
|
||||
let mut bindings = match bindings.check_types(self.db(), &call_arguments, &tcx) {
|
||||
|
||||
@@ -769,6 +769,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
}
|
||||
Type::unknown()
|
||||
}
|
||||
KnownInstanceType::TypedDictSchema(_) => {
|
||||
self.infer_type_expression(slice);
|
||||
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
|
||||
builder.into_diagnostic(format_args!(
|
||||
"`_TypedDictSchema` is not allowed in type expressions",
|
||||
));
|
||||
}
|
||||
Type::unknown()
|
||||
}
|
||||
KnownInstanceType::TypeVar(_) => {
|
||||
self.infer_type_expression(slice);
|
||||
todo_type!("TypeVar annotations")
|
||||
@@ -807,6 +816,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
KnownInstanceType::TypedDictType(_) => {
|
||||
self.infer_type_expression(slice);
|
||||
|
||||
if let Some(builder) = self.context.report_lint(&NON_SUBSCRIPTABLE, subscript) {
|
||||
builder.into_diagnostic(format_args!("Cannot subscript typed dict"));
|
||||
}
|
||||
|
||||
Type::unknown()
|
||||
}
|
||||
KnownInstanceType::TypeAliasType(TypeAliasType::ManualPEP695(_)) => {
|
||||
self.infer_type_expression(slice);
|
||||
todo_type!("Generic manual PEP-695 type alias")
|
||||
@@ -1374,7 +1392,9 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
SpecialFormType::Tuple => {
|
||||
Type::tuple(self.infer_tuple_type_expression(arguments_slice))
|
||||
}
|
||||
SpecialFormType::Generic | SpecialFormType::Protocol => {
|
||||
SpecialFormType::Generic
|
||||
| SpecialFormType::Protocol
|
||||
| SpecialFormType::TypedDictSchema => {
|
||||
self.infer_expression(arguments_slice, TypeContext::default());
|
||||
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
|
||||
builder.into_diagnostic(format_args!(
|
||||
|
||||
@@ -127,10 +127,14 @@ pub enum SpecialFormType {
|
||||
/// Typeshed defines this symbol as a class, but this isn't accurate: it's actually a factory function
|
||||
/// at runtime. We therefore represent it as a special form internally.
|
||||
NamedTuple,
|
||||
|
||||
/// An internal type representing the dictionary literal argument to the functional `TypedDict`
|
||||
/// constructor.
|
||||
TypedDictSchema,
|
||||
}
|
||||
|
||||
impl SpecialFormType {
|
||||
/// Return the [`KnownClass`] which this symbol is an instance of
|
||||
/// Return the [`KnownClass`] which this symbol is an instance of.
|
||||
pub(crate) const fn class(self) -> KnownClass {
|
||||
match self {
|
||||
Self::Annotated
|
||||
@@ -179,7 +183,9 @@ impl SpecialFormType {
|
||||
| Self::ChainMap
|
||||
| Self::OrderedDict => KnownClass::StdlibAlias,
|
||||
|
||||
Self::Unknown | Self::AlwaysTruthy | Self::AlwaysFalsy => KnownClass::Object,
|
||||
Self::Unknown | Self::AlwaysTruthy | Self::AlwaysFalsy | Self::TypedDictSchema => {
|
||||
KnownClass::Object
|
||||
}
|
||||
|
||||
Self::NamedTuple => KnownClass::FunctionType,
|
||||
}
|
||||
@@ -264,6 +270,8 @@ impl SpecialFormType {
|
||||
| Self::Intersection
|
||||
| Self::TypeOf
|
||||
| Self::CallableTypeOf => module.is_ty_extensions(),
|
||||
|
||||
Self::TypedDictSchema => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,7 +332,8 @@ impl SpecialFormType {
|
||||
| Self::ReadOnly
|
||||
| Self::Protocol
|
||||
| Self::Any
|
||||
| Self::Generic => false,
|
||||
| Self::Generic
|
||||
| Self::TypedDictSchema => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,6 +384,7 @@ impl SpecialFormType {
|
||||
SpecialFormType::Protocol => "typing.Protocol",
|
||||
SpecialFormType::Generic => "typing.Generic",
|
||||
SpecialFormType::NamedTuple => "typing.NamedTuple",
|
||||
SpecialFormType::TypedDictSchema => "_TypedDictSchema",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,9 +212,7 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
|
||||
(Type::TypeAlias(_), _) => Ordering::Less,
|
||||
(_, Type::TypeAlias(_)) => Ordering::Greater,
|
||||
|
||||
(Type::TypedDict(left), Type::TypedDict(right)) => {
|
||||
left.defining_class().cmp(&right.defining_class())
|
||||
}
|
||||
(Type::TypedDict(left), Type::TypedDict(right)) => left.cmp(right),
|
||||
(Type::TypedDict(_), _) => Ordering::Less,
|
||||
(_, Type::TypedDict(_)) => Ordering::Greater,
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, Span, SubDiagnostic, SubDiagnosticSeverity};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
@@ -12,6 +14,12 @@ use super::diagnostic::{
|
||||
report_missing_typed_dict_key,
|
||||
};
|
||||
use super::{ApplyTypeMappingVisitor, Type, TypeMapping, visitor};
|
||||
use crate::types::generics::GenericContext;
|
||||
use crate::types::variance::TypeVarVariance;
|
||||
use crate::types::{
|
||||
BoundTypeVarInstance, CallableSignature, CallableType, KnownClass, NormalizedVisitor,
|
||||
Parameter, Parameters, Signature, StringLiteralType, SubclassOfType, Truthiness, UnionType,
|
||||
};
|
||||
use crate::{Db, FxOrderMap};
|
||||
|
||||
use ordermap::OrderSet;
|
||||
@@ -20,7 +28,7 @@ bitflags! {
|
||||
/// Used for `TypedDict` class parameters.
|
||||
/// Keeps track of the keyword arguments that were passed-in during class definition.
|
||||
/// (see https://typing.python.org/en/latest/spec/typeddict.html)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct TypedDictParams: u8 {
|
||||
/// Whether keys are required by default (`total=True`)
|
||||
const TOTAL = 1 << 0;
|
||||
@@ -37,25 +45,41 @@ impl Default for TypedDictParams {
|
||||
|
||||
/// Type that represents the set of all inhabitants (`dict` instances) that conform to
|
||||
/// a given `TypedDict` schema.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, salsa::Update, Hash, get_size2::GetSize)]
|
||||
pub struct TypedDictType<'db> {
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, PartialOrd, Ord, get_size2::GetSize,
|
||||
)]
|
||||
pub enum TypedDictType<'db> {
|
||||
/// A reference to the class (inheriting from `typing.TypedDict`) that specifies the
|
||||
/// schema of this `TypedDict`.
|
||||
defining_class: ClassType<'db>,
|
||||
ClassBased(ClassType<'db>),
|
||||
|
||||
/// A `TypedDict` created using the functional syntax.
|
||||
Functional(FunctionalTypedDictType<'db>),
|
||||
}
|
||||
|
||||
impl<'db> TypedDictType<'db> {
|
||||
pub(crate) fn new(defining_class: ClassType<'db>) -> Self {
|
||||
Self { defining_class }
|
||||
pub(crate) fn from_class(class: ClassType<'db>) -> Self {
|
||||
TypedDictType::ClassBased(class)
|
||||
}
|
||||
|
||||
pub(crate) fn defining_class(self) -> ClassType<'db> {
|
||||
self.defining_class
|
||||
pub(crate) fn items(&self, db: &'db dyn Db) -> Cow<'db, FxOrderMap<Name, Field<'db>>> {
|
||||
match self {
|
||||
TypedDictType::Functional(functional) => Cow::Borrowed(functional.items(db)),
|
||||
TypedDictType::ClassBased(class) => {
|
||||
let (class_literal, specialization) = class.class_literal(db);
|
||||
Cow::Owned(class_literal.fields(db, specialization, CodeGeneratorKind::TypedDict))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn items(self, db: &'db dyn Db) -> FxOrderMap<Name, Field<'db>> {
|
||||
let (class_literal, specialization) = self.defining_class.class_literal(db);
|
||||
class_literal.fields(db, specialization, CodeGeneratorKind::TypedDict)
|
||||
/// Return the meta-type of this `TypedDict` type.
|
||||
pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> {
|
||||
// `TypedDict` instances are instances of `dict` at runtime, but its important that we
|
||||
// understand a more specific meta type in order to correctly handle `__getitem__`.
|
||||
match self {
|
||||
TypedDictType::ClassBased(class) => SubclassOfType::from(db, class),
|
||||
TypedDictType::Functional(_) => KnownClass::TypedDictFallback.to_class_literal(db),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn apply_type_mapping_impl<'a>(
|
||||
@@ -65,12 +89,378 @@ impl<'db> TypedDictType<'db> {
|
||||
visitor: &ApplyTypeMappingVisitor<'db>,
|
||||
) -> Self {
|
||||
// TODO: Materialization of gradual TypedDicts needs more logic
|
||||
Self {
|
||||
defining_class: self
|
||||
.defining_class
|
||||
.apply_type_mapping_impl(db, type_mapping, visitor),
|
||||
match self {
|
||||
TypedDictType::ClassBased(class) => {
|
||||
TypedDictType::ClassBased(class.apply_type_mapping_impl(db, type_mapping, visitor))
|
||||
}
|
||||
TypedDictType::Functional(functional) => TypedDictType::Functional(
|
||||
functional.apply_type_mapping_impl(db, type_mapping, visitor),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn normalized_impl(
|
||||
self,
|
||||
_db: &'db dyn Db,
|
||||
_visitor: &NormalizedVisitor<'db>,
|
||||
) -> Self {
|
||||
// TODO: Normalize typed dicts.
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the type of a synthesized member like `__setitem__` or `__getitem__` for a `TypedDict`.
|
||||
pub(crate) fn synthesized_member(
|
||||
db: &'db dyn Db,
|
||||
instance_ty: Type<'db>,
|
||||
fields: &FxOrderMap<Name, Field<'db>>,
|
||||
name: &str,
|
||||
) -> Option<Type<'db>> {
|
||||
match name {
|
||||
"__setitem__" => {
|
||||
// Add (key type, value type) overloads for all TypedDict items ("fields") that are not read-only:
|
||||
let mut writeable_fields = fields
|
||||
.iter()
|
||||
.filter(|(_, field)| !field.is_read_only())
|
||||
.peekable();
|
||||
|
||||
if writeable_fields.peek().is_none() {
|
||||
// If there are no writeable fields, synthesize a `__setitem__` that takes
|
||||
// a `key` of type `Never` to signal that no keys are accepted. This leads
|
||||
// to slightly more user-friendly error messages compared to returning an
|
||||
// empty overload set.
|
||||
return Some(Type::Callable(CallableType::new(
|
||||
db,
|
||||
CallableSignature::single(Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(Type::Never),
|
||||
Parameter::positional_only(Some(Name::new_static("value")))
|
||||
.with_annotated_type(Type::any()),
|
||||
]),
|
||||
Some(Type::none(db)),
|
||||
)),
|
||||
true,
|
||||
)));
|
||||
}
|
||||
|
||||
let overloads = writeable_fields.map(|(name, field)| {
|
||||
let key_type = Type::StringLiteral(StringLiteralType::new(db, name.as_str()));
|
||||
|
||||
Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(key_type),
|
||||
Parameter::positional_only(Some(Name::new_static("value")))
|
||||
.with_annotated_type(field.declared_ty),
|
||||
]),
|
||||
Some(Type::none(db)),
|
||||
)
|
||||
});
|
||||
|
||||
Some(Type::Callable(CallableType::new(
|
||||
db,
|
||||
CallableSignature::from_overloads(overloads),
|
||||
true,
|
||||
)))
|
||||
}
|
||||
"__getitem__" => {
|
||||
// Add (key -> value type) overloads for all TypedDict items ("fields"):
|
||||
let overloads = fields.iter().map(|(name, field)| {
|
||||
let key_type = Type::StringLiteral(StringLiteralType::new(db, name.as_str()));
|
||||
|
||||
Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(key_type),
|
||||
]),
|
||||
Some(field.declared_ty),
|
||||
)
|
||||
});
|
||||
|
||||
Some(Type::Callable(CallableType::new(
|
||||
db,
|
||||
CallableSignature::from_overloads(overloads),
|
||||
true,
|
||||
)))
|
||||
}
|
||||
"get" => {
|
||||
let overloads = fields
|
||||
.into_iter()
|
||||
.flat_map(|(name, field)| {
|
||||
let key_type =
|
||||
Type::StringLiteral(StringLiteralType::new(db, name.as_str()));
|
||||
|
||||
// For a required key, `.get()` always returns the value type. For a non-required key,
|
||||
// `.get()` returns the union of the value type and the type of the default argument
|
||||
// (which defaults to `None`).
|
||||
|
||||
// TODO: For now, we use two overloads here. They can be merged into a single function
|
||||
// once the generics solver takes default arguments into account.
|
||||
|
||||
let get_sig = Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(key_type),
|
||||
]),
|
||||
Some(if field.is_required() {
|
||||
field.declared_ty
|
||||
} else {
|
||||
UnionType::from_elements(db, [field.declared_ty, Type::none(db)])
|
||||
}),
|
||||
);
|
||||
|
||||
let t_default =
|
||||
BoundTypeVarInstance::synthetic(db, "T", TypeVarVariance::Covariant);
|
||||
|
||||
let get_with_default_sig = Signature::new_generic(
|
||||
Some(GenericContext::from_typevar_instances(db, [t_default])),
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(key_type),
|
||||
Parameter::positional_only(Some(Name::new_static("default")))
|
||||
.with_annotated_type(Type::TypeVar(t_default)),
|
||||
]),
|
||||
Some(if field.is_required() {
|
||||
field.declared_ty
|
||||
} else {
|
||||
UnionType::from_elements(
|
||||
db,
|
||||
[field.declared_ty, Type::TypeVar(t_default)],
|
||||
)
|
||||
}),
|
||||
);
|
||||
|
||||
[get_sig, get_with_default_sig]
|
||||
})
|
||||
// Fallback overloads for unknown keys
|
||||
.chain(std::iter::once({
|
||||
Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(KnownClass::Str.to_instance(db)),
|
||||
]),
|
||||
Some(UnionType::from_elements(
|
||||
db,
|
||||
[Type::unknown(), Type::none(db)],
|
||||
)),
|
||||
)
|
||||
}))
|
||||
.chain(std::iter::once({
|
||||
let t_default =
|
||||
BoundTypeVarInstance::synthetic(db, "T", TypeVarVariance::Covariant);
|
||||
|
||||
Signature::new_generic(
|
||||
Some(GenericContext::from_typevar_instances(db, [t_default])),
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(KnownClass::Str.to_instance(db)),
|
||||
Parameter::positional_only(Some(Name::new_static("default")))
|
||||
.with_annotated_type(Type::TypeVar(t_default)),
|
||||
]),
|
||||
Some(UnionType::from_elements(
|
||||
db,
|
||||
[Type::unknown(), Type::TypeVar(t_default)],
|
||||
)),
|
||||
)
|
||||
}));
|
||||
|
||||
Some(Type::Callable(CallableType::new(
|
||||
db,
|
||||
CallableSignature::from_overloads(overloads),
|
||||
true,
|
||||
)))
|
||||
}
|
||||
"pop" => {
|
||||
let overloads = fields
|
||||
.iter()
|
||||
.filter(|(_, field)| {
|
||||
// Only synthesize `pop` for fields that are not required.
|
||||
!field.is_required()
|
||||
})
|
||||
.flat_map(|(name, field)| {
|
||||
let key_type =
|
||||
Type::StringLiteral(StringLiteralType::new(db, name.as_str()));
|
||||
|
||||
// TODO: Similar to above: consider merging these two overloads into one
|
||||
|
||||
// `.pop()` without default
|
||||
let pop_sig = Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(key_type),
|
||||
]),
|
||||
Some(field.declared_ty),
|
||||
);
|
||||
|
||||
// `.pop()` with a default value
|
||||
let t_default =
|
||||
BoundTypeVarInstance::synthetic(db, "T", TypeVarVariance::Covariant);
|
||||
|
||||
let pop_with_default_sig = Signature::new_generic(
|
||||
Some(GenericContext::from_typevar_instances(db, [t_default])),
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(key_type),
|
||||
Parameter::positional_only(Some(Name::new_static("default")))
|
||||
.with_annotated_type(Type::TypeVar(t_default)),
|
||||
]),
|
||||
Some(UnionType::from_elements(
|
||||
db,
|
||||
[field.declared_ty, Type::TypeVar(t_default)],
|
||||
)),
|
||||
);
|
||||
|
||||
[pop_sig, pop_with_default_sig]
|
||||
});
|
||||
|
||||
Some(Type::Callable(CallableType::new(
|
||||
db,
|
||||
CallableSignature::from_overloads(overloads),
|
||||
true,
|
||||
)))
|
||||
}
|
||||
"setdefault" => {
|
||||
let overloads = fields.iter().map(|(name, field)| {
|
||||
let key_type = Type::StringLiteral(StringLiteralType::new(db, name.as_str()));
|
||||
|
||||
// `setdefault` always returns the field type
|
||||
Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::positional_only(Some(Name::new_static("key")))
|
||||
.with_annotated_type(key_type),
|
||||
Parameter::positional_only(Some(Name::new_static("default")))
|
||||
.with_annotated_type(field.declared_ty),
|
||||
]),
|
||||
Some(field.declared_ty),
|
||||
)
|
||||
});
|
||||
|
||||
Some(Type::Callable(CallableType::new(
|
||||
db,
|
||||
CallableSignature::from_overloads(overloads),
|
||||
true,
|
||||
)))
|
||||
}
|
||||
"update" => {
|
||||
// TODO: synthesize a set of overloads with precise types
|
||||
let signature = Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("self")))
|
||||
.with_annotated_type(instance_ty),
|
||||
Parameter::variadic(Name::new_static("args")),
|
||||
Parameter::keyword_variadic(Name::new_static("kwargs")),
|
||||
]),
|
||||
Some(Type::none(db)),
|
||||
);
|
||||
|
||||
Some(CallableType::function_like(db, signature))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An internal type representing the dictionary literal argument to the functional `TypedDict`
|
||||
/// constructor.
|
||||
#[salsa::interned(debug, heap_size=TypedDictSchema::heap_size)]
|
||||
#[derive(PartialOrd, Ord)]
|
||||
pub struct TypedDictSchema<'db> {
|
||||
pub(crate) items: FxOrderMap<Name, TypedDictSchemaField<'db>>,
|
||||
}
|
||||
|
||||
// The Salsa heap is tracked separately.
|
||||
impl get_size2::GetSize for TypedDictSchema<'_> {}
|
||||
|
||||
impl<'db> TypedDictSchema<'db> {
|
||||
fn heap_size((items,): &(FxOrderMap<Name, TypedDictSchemaField<'db>>,)) -> usize {
|
||||
ruff_memory_usage::order_map_heap_size(items)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||
pub struct TypedDictSchemaField<'db> {
|
||||
/// The declared type of the field
|
||||
pub(crate) declared_ty: Type<'db>,
|
||||
|
||||
/// Whether this field is required.
|
||||
pub(crate) is_required: Truthiness,
|
||||
|
||||
/// Whether this field is marked read-only.
|
||||
pub(crate) is_read_only: bool,
|
||||
}
|
||||
|
||||
#[salsa::interned(debug, heap_size=FunctionalTypedDictType::heap_size)]
|
||||
#[derive(PartialOrd, Ord)]
|
||||
pub struct FunctionalTypedDictType<'db> {
|
||||
pub(crate) name: Name,
|
||||
|
||||
pub(crate) params: TypedDictParams,
|
||||
|
||||
#[returns(ref)]
|
||||
pub(crate) items: FxOrderMap<Name, Field<'db>>,
|
||||
}
|
||||
|
||||
// The Salsa heap is tracked separately.
|
||||
impl get_size2::GetSize for FunctionalTypedDictType<'_> {}
|
||||
|
||||
impl<'db> FunctionalTypedDictType<'db> {
|
||||
pub(super) fn apply_type_mapping_impl<'a>(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
type_mapping: &TypeMapping<'a, 'db>,
|
||||
visitor: &ApplyTypeMappingVisitor<'db>,
|
||||
) -> Self {
|
||||
let items = self
|
||||
.items(db)
|
||||
.iter()
|
||||
.map(|(name, field)| {
|
||||
let field = field
|
||||
.clone()
|
||||
.apply_type_mapping_impl(db, type_mapping, visitor);
|
||||
|
||||
(name.clone(), field)
|
||||
})
|
||||
.collect::<FxOrderMap<_, _>>();
|
||||
|
||||
FunctionalTypedDictType::new(db, self.name(db), self.params(db), items)
|
||||
}
|
||||
|
||||
pub(crate) fn normalized_impl(
|
||||
self,
|
||||
_db: &'db dyn Db,
|
||||
_visitor: &NormalizedVisitor<'db>,
|
||||
) -> Self {
|
||||
// TODO: Normalize typed dicts.
|
||||
self
|
||||
}
|
||||
|
||||
fn heap_size(
|
||||
(name, params, items): &(Name, TypedDictParams, FxOrderMap<Name, Field<'db>>),
|
||||
) -> usize {
|
||||
ruff_memory_usage::heap_size(name)
|
||||
+ ruff_memory_usage::heap_size(params)
|
||||
+ ruff_memory_usage::order_map_heap_size(items)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn walk_typed_dict_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
|
||||
@@ -78,7 +468,14 @@ pub(crate) fn walk_typed_dict_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
|
||||
typed_dict: TypedDictType<'db>,
|
||||
visitor: &V,
|
||||
) {
|
||||
visitor.visit_type(db, typed_dict.defining_class.into());
|
||||
match typed_dict {
|
||||
TypedDictType::ClassBased(class) => visitor.visit_type(db, class.into()),
|
||||
TypedDictType::Functional(functional) => {
|
||||
for (_, item) in functional.items(db) {
|
||||
visitor.visit_type(db, item.declared_ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn typed_dict_params_from_class_def(class_stmt: &StmtClassDef) -> TypedDictParams {
|
||||
|
||||
Reference in New Issue
Block a user