Compare commits

...

9 Commits

Author SHA1 Message Date
Ibraheem Ahmed
b22cc1aa2b nits 2025-10-07 20:09:30 -04:00
Ibraheem Ahmed
26c25bec82 avoid false positives when inheriting from functional TypedDicts 2025-10-07 17:21:20 -04:00
Ibraheem Ahmed
2949f76b47 support dictionary literal TypedDict constructors 2025-10-07 16:44:51 -04:00
Ibraheem Ahmed
b753851379 use internal TypedDictSchema type for functional TypedDict constructor 2025-10-07 15:35:33 -04:00
Ibraheem Ahmed
f2a25b0fd7 format 2025-10-07 14:50:09 -04:00
Ibraheem Ahmed
6981500d93 use infer_annotation_expression for TypedDict fields 2025-10-07 14:47:34 -04:00
Ibraheem Ahmed
6fdd4c3334 reduce size of Type 2025-10-07 14:46:09 -04:00
Ibraheem Ahmed
2959ff19bc nits 2025-10-07 13:37:15 -04:00
Ibraheem Ahmed
98a0b77174 add support for functional TypedDict syntax 2025-10-06 23:56:26 -04:00
13 changed files with 1143 additions and 417 deletions

View File

@@ -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())
}

View File

@@ -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): ...
```

View File

@@ -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

View File

@@ -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),

View File

@@ -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,

View File

@@ -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,
}
}

View File

@@ -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()),

View File

@@ -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)),
}
}

View File

@@ -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) {

View File

@@ -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!(

View File

@@ -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",
}
}
}

View File

@@ -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,

View File

@@ -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 {