mirror of https://github.com/astral-sh/ruff
[ty] New `Type` variant for `TypedDict` (#19733)
## Summary This PR adds a new `Type::TypedDict` variant. Before this PR, we treated `TypedDict`-based types as dynamic Todo-types, and I originally planned to make this change a no-op. And we do in fact still treat that new variant similar to a dynamic type when it comes to type properties such as assignability and subtyping. But then I somehow tricked myself into implementing some of the things correctly, so here we are. The two main behavioral changes are: (1) we now also detect generic `TypedDict`s, which removes a few false positives in the ecosystem, and (2) we now support *attribute* access (not key-based indexing!) on these types, i.e. we infer proper types for something like `MyTypedDict.__required_keys__`. Nothing exciting yet, but gets the infrastructure into place. Note that with this PR, the type of (the type) `MyTypedDict` itself is still represented as a `Type::ClassLiteral` or `Type::GenericAlias` (in case `MyTypedDict` is generic). Only inhabitants of `MyTypedDict` (instances of `dict` at runtime) are represented by `Type::TypedDict`. We may want to revisit this decision in the future, if this turns out to be too error-prone. Right now, we need to use `.is_typed_dict(db)` in all the right places to distinguish between actual (generic) classes and `TypedDict`s. But so far, it seemed unnecessary to add additional `Type` variants for these as well. part of https://github.com/astral-sh/ty/issues/154 ## Ecosystem impact The new diagnostics on `cloud-init` look like true positives to me. ## Test Plan Updated and new Markdown tests
This commit is contained in:
parent
351121c5c5
commit
14fbc2b167
|
|
@ -3441,7 +3441,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
|||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.23.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=86ca4a9d70e97dd5107e6111a09647dd10ff7535#86ca4a9d70e97dd5107e6111a09647dd10ff7535"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=d66fe331d546216132ace503512b94d5c68d2c50#d66fe331d546216132ace503512b94d5c68d2c50"
|
||||
dependencies = [
|
||||
"boxcar",
|
||||
"compact_str",
|
||||
|
|
@ -3465,12 +3465,12 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.23.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=86ca4a9d70e97dd5107e6111a09647dd10ff7535#86ca4a9d70e97dd5107e6111a09647dd10ff7535"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=d66fe331d546216132ace503512b94d5c68d2c50#d66fe331d546216132ace503512b94d5c68d2c50"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.23.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=86ca4a9d70e97dd5107e6111a09647dd10ff7535#86ca4a9d70e97dd5107e6111a09647dd10ff7535"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=d66fe331d546216132ace503512b94d5c68d2c50#d66fe331d546216132ace503512b94d5c68d2c50"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ regex-automata = { version = "0.4.9" }
|
|||
rustc-hash = { version = "2.0.0" }
|
||||
rustc-stable-hash = { version = "0.1.2" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "86ca4a9d70e97dd5107e6111a09647dd10ff7535", default-features = false, features = [
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "d66fe331d546216132ace503512b94d5c68d2c50", default-features = false, features = [
|
||||
"compact_str",
|
||||
"macros",
|
||||
"salsa_unstable",
|
||||
|
|
|
|||
|
|
@ -208,7 +208,28 @@ static_assert(has_member(Answer, "YES"))
|
|||
static_assert(has_member(Answer, "__members__"))
|
||||
```
|
||||
|
||||
### Unions
|
||||
### TypedDicts
|
||||
|
||||
```py
|
||||
from ty_extensions import has_member, static_assert
|
||||
from typing import TypedDict
|
||||
|
||||
class Person(TypedDict):
|
||||
name: str
|
||||
age: int | None
|
||||
|
||||
static_assert(not has_member(Person, "name"))
|
||||
static_assert(not has_member(Person, "age"))
|
||||
|
||||
static_assert(has_member(Person, "__total__"))
|
||||
static_assert(has_member(Person, "__required_keys__"))
|
||||
|
||||
def _(person: Person):
|
||||
static_assert(not has_member(person, "name"))
|
||||
static_assert(not has_member(person, "age"))
|
||||
|
||||
static_assert(has_member(person, "keys"))
|
||||
```
|
||||
|
||||
For unions, `ide_support::all_members` only returns members that are available on all elements of
|
||||
the union.
|
||||
|
|
|
|||
|
|
@ -205,3 +205,54 @@ reveal_type(bool(AmbiguousEnum2.YES)) # revealed: bool
|
|||
reveal_type(bool(CustomLenEnum.NO)) # revealed: bool
|
||||
reveal_type(bool(CustomLenEnum.YES)) # revealed: bool
|
||||
```
|
||||
|
||||
## TypedDict
|
||||
|
||||
It may be feasible to infer `Literal[True]` for some `TypedDict` types, if `{}` can definitely be
|
||||
excluded as a possible value. We currently do not attempt to do this.
|
||||
|
||||
If `{}` is the *only* possible value, we could infer `Literal[False]`. This might only be possible
|
||||
if something like <https://peps.python.org/pep-0728> is accepted, a `TypedDict` has no defined
|
||||
items, and `closed=True` is used.
|
||||
|
||||
```py
|
||||
from typing_extensions import TypedDict, Literal, NotRequired
|
||||
|
||||
class Normal(TypedDict):
|
||||
a: str
|
||||
b: int
|
||||
|
||||
def _(n: Normal) -> None:
|
||||
# Could be `Literal[True]`
|
||||
reveal_type(bool(n)) # revealed: bool
|
||||
|
||||
class OnlyFalsyItems(TypedDict):
|
||||
wrong: Literal[False]
|
||||
|
||||
def _(n: OnlyFalsyItems) -> None:
|
||||
# Could be `Literal[True]` (it does not matter if all items are falsy)
|
||||
reveal_type(bool(n)) # revealed: bool
|
||||
|
||||
class Empty(TypedDict):
|
||||
pass
|
||||
|
||||
def _(e: Empty) -> None:
|
||||
# This should be `bool`. `Literal[False]` would be wrong, as `Empty` can be subclassed.
|
||||
reveal_type(bool(e)) # revealed: bool
|
||||
|
||||
class AllKeysOptional(TypedDict, total=False):
|
||||
a: str
|
||||
b: int
|
||||
|
||||
def _(a: AllKeysOptional) -> None:
|
||||
# This should be `bool`. `Literal[True]` would be wrong as `{}` is a valid value.
|
||||
reveal_type(bool(a)) # revealed: bool
|
||||
|
||||
class AllKeysNotRequired(TypedDict):
|
||||
a: NotRequired[str]
|
||||
b: NotRequired[int]
|
||||
|
||||
def _(a: AllKeysNotRequired) -> None:
|
||||
# This should be `bool`. `Literal[True]` would be wrong as `{}` is a valid value.
|
||||
reveal_type(bool(a)) # revealed: bool
|
||||
```
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ Assignments to keys are also validated:
|
|||
```py
|
||||
# TODO: this should be an error
|
||||
alice["name"] = None
|
||||
# TODO: this should be an error
|
||||
bob["name"] = None
|
||||
```
|
||||
|
||||
Assignments to non-existing keys are disallowed:
|
||||
|
|
@ -67,6 +69,8 @@ Assignments to non-existing keys are disallowed:
|
|||
```py
|
||||
# TODO: this should be an error
|
||||
alice["extra"] = True
|
||||
# TODO: this should be an error
|
||||
bob["extra"] = True
|
||||
```
|
||||
|
||||
## Structural assignability
|
||||
|
|
@ -123,7 +127,7 @@ dangerous(alice)
|
|||
reveal_type(alice["name"]) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Types of keys and values
|
||||
## Methods on `TypedDict`
|
||||
|
||||
```py
|
||||
from typing import TypedDict
|
||||
|
|
@ -133,8 +137,13 @@ class Person(TypedDict):
|
|||
age: int | None
|
||||
|
||||
def _(p: Person) -> None:
|
||||
reveal_type(p.keys()) # revealed: @Todo(Support for `TypedDict`)
|
||||
reveal_type(p.values()) # revealed: @Todo(Support for `TypedDict`)
|
||||
reveal_type(p.keys()) # revealed: dict_keys[str, object]
|
||||
reveal_type(p.values()) # revealed: dict_values[str, object]
|
||||
|
||||
reveal_type(p.setdefault("name", "Alice")) # revealed: @Todo(Support for `TypedDict`)
|
||||
|
||||
reveal_type(p.get("name")) # revealed: @Todo(Support for `TypedDict`)
|
||||
reveal_type(p.get("name", "Unknown")) # revealed: @Todo(Support for `TypedDict`)
|
||||
```
|
||||
|
||||
## Unlike normal classes
|
||||
|
|
@ -149,11 +158,16 @@ class Person(TypedDict):
|
|||
name: str
|
||||
age: int | None
|
||||
|
||||
# TODO: this should be an error
|
||||
# error: [unresolved-attribute] "Type `<class 'Person'>` has no attribute `name`"
|
||||
Person.name
|
||||
|
||||
# TODO: this should be an error
|
||||
Person(name="Alice", age=30).name
|
||||
def _(P: type[Person]):
|
||||
# error: [unresolved-attribute] "Type `type[Person]` has no attribute `name`"
|
||||
P.name
|
||||
|
||||
def _(p: Person) -> None:
|
||||
# error: [unresolved-attribute] "Type `Person` has no attribute `name`"
|
||||
p.name
|
||||
```
|
||||
|
||||
## Special properties
|
||||
|
|
@ -167,9 +181,29 @@ class Person(TypedDict):
|
|||
name: str
|
||||
age: int | None
|
||||
|
||||
reveal_type(Person.__total__) # revealed: @Todo(Support for `TypedDict`)
|
||||
reveal_type(Person.__required_keys__) # revealed: @Todo(Support for `TypedDict`)
|
||||
reveal_type(Person.__optional_keys__) # revealed: @Todo(Support for `TypedDict`)
|
||||
reveal_type(Person.__total__) # revealed: bool
|
||||
reveal_type(Person.__required_keys__) # revealed: frozenset[str]
|
||||
reveal_type(Person.__optional_keys__) # revealed: frozenset[str]
|
||||
```
|
||||
|
||||
These attributes can not be accessed on inhabitants:
|
||||
|
||||
```py
|
||||
def _(person: Person) -> None:
|
||||
# TODO: these should be errors
|
||||
person.__total__
|
||||
person.__required_keys__
|
||||
person.__optional_keys__
|
||||
```
|
||||
|
||||
Also, they can not be accessed on `type(person)`, as that would be `dict` at runtime:
|
||||
|
||||
```py
|
||||
def _(t_person: type[Person]) -> None:
|
||||
# TODO: these should be errors
|
||||
t_person.__total__
|
||||
t_person.__required_keys__
|
||||
t_person.__optional_keys__
|
||||
```
|
||||
|
||||
## Subclassing
|
||||
|
|
@ -272,6 +306,9 @@ msg = Message(id=1, content="Hello")
|
|||
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
|
||||
```
|
||||
|
||||
[`typeddict`]: https://typing.python.org/en/latest/spec/typeddict.html
|
||||
|
|
|
|||
|
|
@ -223,7 +223,8 @@ impl<'db> Completion<'db> {
|
|||
Type::NominalInstance(_)
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::Tuple(_)
|
||||
| Type::BoundSuper(_) => CompletionKind::Struct,
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypedDict(_) => CompletionKind::Struct,
|
||||
Type::IntLiteral(_)
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::TypeIs(_)
|
||||
|
|
|
|||
|
|
@ -606,6 +606,8 @@ pub enum Type<'db> {
|
|||
BoundSuper(BoundSuperType<'db>),
|
||||
/// A subtype of `bool` that allows narrowing in both positive and negative cases.
|
||||
TypeIs(TypeIsType<'db>),
|
||||
/// A type that represents an inhabitant of a `TypedDict`.
|
||||
TypedDict(TypedDictType<'db>),
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
|
|
@ -780,6 +782,10 @@ impl<'db> Type<'db> {
|
|||
Type::TypeIs(type_is) => {
|
||||
type_is.with_type(db, type_is.return_type(db).materialize(db, variance))
|
||||
}
|
||||
Type::TypedDict(_) => {
|
||||
// TODO: Materialization of gradual TypedDicts
|
||||
*self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1080,6 +1086,12 @@ 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::LiteralString
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy
|
||||
|
|
@ -1128,7 +1140,8 @@ impl<'db> Type<'db> {
|
|||
| Type::AlwaysTruthy
|
||||
| Type::PropertyInstance(_)
|
||||
// might inherit `Any`, but subtyping is still reflexive
|
||||
| Type::ClassLiteral(_) => true,
|
||||
| Type::ClassLiteral(_)
|
||||
=> true,
|
||||
Type::Dynamic(_)
|
||||
| Type::NominalInstance(_)
|
||||
| Type::ProtocolInstance(_)
|
||||
|
|
@ -1140,7 +1153,8 @@ impl<'db> Type<'db> {
|
|||
| Type::Tuple(_)
|
||||
| Type::TypeVar(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypeIs(_) => false,
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1203,7 +1217,8 @@ impl<'db> Type<'db> {
|
|||
| Type::LiteralString
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::Tuple(_)
|
||||
| Type::TypeIs(_) => None,
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_) => None,
|
||||
|
||||
// TODO
|
||||
Type::MethodWrapper(_)
|
||||
|
|
@ -1297,6 +1312,11 @@ impl<'db> Type<'db> {
|
|||
field.default_type(db).has_relation_to(db, right, relation)
|
||||
}
|
||||
|
||||
(Type::TypedDict(_), _) | (_, Type::TypedDict(_)) => {
|
||||
// TODO: Implement assignability and subtyping for TypedDict
|
||||
relation.is_assignability()
|
||||
}
|
||||
|
||||
// In general, a TypeVar `T` is not a subtype of a type `S` unless one of the two conditions is satisfied:
|
||||
// 1. `T` is a bound TypeVar and `T`'s upper bound is a subtype of `S`.
|
||||
// TypeVars without an explicit upper bound are treated as having an implicit upper bound of `object`.
|
||||
|
|
@ -1761,6 +1781,11 @@ impl<'db> Type<'db> {
|
|||
|
||||
(Type::Dynamic(_), _) | (_, Type::Dynamic(_)) => false,
|
||||
|
||||
(Type::TypedDict(_), _) | (_, Type::TypedDict(_)) => {
|
||||
// TODO: Implement disjointness for TypedDict
|
||||
false
|
||||
}
|
||||
|
||||
// A typevar is never disjoint from itself, since all occurrences of the typevar must
|
||||
// be specialized to the same type. (This is an important difference between typevars
|
||||
// and `Any`!) Different typevars might be disjoint, depending on their bounds and
|
||||
|
|
@ -2374,6 +2399,7 @@ impl<'db> Type<'db> {
|
|||
}
|
||||
Type::AlwaysTruthy | Type::AlwaysFalsy => false,
|
||||
Type::TypeIs(type_is) => type_is.is_bound(db),
|
||||
Type::TypedDict(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2462,7 +2488,8 @@ impl<'db> Type<'db> {
|
|||
| Type::Callable(_)
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::DataclassDecorator(_)
|
||||
| Type::DataclassTransformer(_) => false,
|
||||
| Type::DataclassTransformer(_)
|
||||
| Type::TypedDict(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2502,6 +2529,10 @@ impl<'db> Type<'db> {
|
|||
|
||||
Type::Dynamic(_) | Type::Never => Some(Place::bound(self).into()),
|
||||
|
||||
Type::ClassLiteral(class) if class.is_typed_dict(db) => {
|
||||
Some(class.typed_dict_member(db, None, name, policy))
|
||||
}
|
||||
|
||||
Type::ClassLiteral(class) => {
|
||||
match (class.known(db), name) {
|
||||
(Some(KnownClass::FunctionType), "__get__") => Some(
|
||||
|
|
@ -2531,6 +2562,10 @@ impl<'db> Type<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
Type::GenericAlias(alias) if alias.is_typed_dict(db) => {
|
||||
Some(alias.origin(db).typed_dict_member(db, None, name, policy))
|
||||
}
|
||||
|
||||
Type::GenericAlias(alias) => {
|
||||
Some(ClassType::from(*alias).class_member(db, name, policy))
|
||||
}
|
||||
|
|
@ -2582,7 +2617,8 @@ impl<'db> Type<'db> {
|
|||
| Type::NominalInstance(_)
|
||||
| Type::ProtocolInstance(_)
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::TypeIs(_) => None,
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2739,6 +2775,8 @@ impl<'db> Type<'db> {
|
|||
Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => {
|
||||
Place::Unbound.into()
|
||||
}
|
||||
|
||||
Type::TypedDict(_) => Place::Unbound.into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3228,7 +3266,8 @@ impl<'db> Type<'db> {
|
|||
| Type::FunctionLiteral(..)
|
||||
| Type::AlwaysTruthy
|
||||
| Type::AlwaysFalsy
|
||||
| Type::TypeIs(..) => {
|
||||
| Type::TypeIs(..)
|
||||
| Type::TypedDict(_) => {
|
||||
let fallback = self.instance_member(db, name_str);
|
||||
|
||||
let result = self.invoke_descriptor_protocol(
|
||||
|
|
@ -3514,6 +3553,12 @@ impl<'db> Type<'db> {
|
|||
| Type::LiteralString
|
||||
| Type::TypeIs(_) => Truthiness::Ambiguous,
|
||||
|
||||
Type::TypedDict(_) => {
|
||||
// TODO: We could do better here, but it's unclear if this is important.
|
||||
// See existing `TypedDict`-related tests in `truthiness.md`
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
|
||||
Type::FunctionLiteral(_)
|
||||
| Type::BoundMethod(_)
|
||||
| Type::WrapperDescriptor(_)
|
||||
|
|
@ -4552,7 +4597,8 @@ impl<'db> Type<'db> {
|
|||
| Type::Tuple(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::TypeIs(_) => CallableBinding::not_callable(self).into(),
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_) => CallableBinding::not_callable(self).into(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5167,7 +5213,8 @@ impl<'db> Type<'db> {
|
|||
| Type::BoundSuper(_)
|
||||
| Type::AlwaysTruthy
|
||||
| Type::AlwaysFalsy
|
||||
| Type::TypeIs(_) => None,
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5206,10 +5253,16 @@ impl<'db> Type<'db> {
|
|||
KnownClass::Float.to_instance(db),
|
||||
],
|
||||
),
|
||||
_ if class.is_typed_dict(db) => {
|
||||
Type::TypedDict(TypedDictType::new(db, ClassType::NonGeneric(*class)))
|
||||
}
|
||||
_ => Type::instance(db, class.default_specialization(db)),
|
||||
};
|
||||
Ok(ty)
|
||||
}
|
||||
Type::GenericAlias(alias) if alias.is_typed_dict(db) => Ok(Type::TypedDict(
|
||||
TypedDictType::new(db, ClassType::from(*alias)),
|
||||
)),
|
||||
Type::GenericAlias(alias) => Ok(Type::instance(db, ClassType::from(*alias))),
|
||||
|
||||
Type::SubclassOf(_)
|
||||
|
|
@ -5235,7 +5288,8 @@ impl<'db> Type<'db> {
|
|||
| Type::BoundSuper(_)
|
||||
| Type::ProtocolInstance(_)
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::TypeIs(_) => Err(InvalidTypeExpressionError {
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_) => Err(InvalidTypeExpressionError {
|
||||
invalid_expressions: smallvec::smallvec_inline![
|
||||
InvalidTypeExpression::InvalidType(*self, scope_id)
|
||||
],
|
||||
|
|
@ -5537,6 +5591,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),
|
||||
Type::TypedDict(typed_dict) => SubclassOfType::from(db, typed_dict.defining_class(db)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5637,6 +5692,10 @@ impl<'db> Type<'db> {
|
|||
Type::GenericAlias(generic.apply_type_mapping(db, type_mapping))
|
||||
}
|
||||
|
||||
Type::TypedDict(typed_dict) => {
|
||||
Type::TypedDict(typed_dict.apply_type_mapping(db, type_mapping))
|
||||
}
|
||||
|
||||
Type::SubclassOf(subclass_of) => Type::SubclassOf(
|
||||
subclass_of.apply_type_mapping(db, type_mapping),
|
||||
),
|
||||
|
|
@ -5796,7 +5855,8 @@ impl<'db> Type<'db> {
|
|||
| Type::EnumLiteral(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::SpecialForm(_)
|
||||
| Type::KnownInstance(_) => {}
|
||||
| Type::KnownInstance(_)
|
||||
| Type::TypedDict(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5911,6 +5971,10 @@ impl<'db> Type<'db> {
|
|||
Protocol::Synthesized(_) => None,
|
||||
},
|
||||
|
||||
Type::TypedDict(typed_dict) => {
|
||||
Some(TypeDefinition::Class(typed_dict.defining_class(db).definition(db)))
|
||||
}
|
||||
|
||||
Self::Union(_) | Self::Intersection(_) => None,
|
||||
|
||||
// These types have no definition
|
||||
|
|
@ -6255,9 +6319,6 @@ pub enum DynamicType {
|
|||
/// A special Todo-variant for type aliases declared using `typing.TypeAlias`.
|
||||
/// A temporary variant to detect and special-case the handling of these aliases in autocomplete suggestions.
|
||||
TodoTypeAlias,
|
||||
/// A special Todo-variant for classes inheriting from `TypedDict`.
|
||||
/// A temporary variant to avoid false positives while we wait for full support.
|
||||
TodoTypedDict,
|
||||
}
|
||||
|
||||
impl DynamicType {
|
||||
|
|
@ -6289,13 +6350,6 @@ impl std::fmt::Display for DynamicType {
|
|||
f.write_str("@Todo")
|
||||
}
|
||||
}
|
||||
DynamicType::TodoTypedDict => {
|
||||
if cfg!(debug_assertions) {
|
||||
f.write_str("@Todo(Support for `TypedDict`)")
|
||||
} else {
|
||||
f.write_str("@Todo")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8830,6 +8884,40 @@ impl<'db> EnumLiteralType<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Type that represents the set of all inhabitants (`dict` instances) that conform to
|
||||
/// a given `TypedDict` schema.
|
||||
#[salsa::interned(debug)]
|
||||
#[derive(PartialOrd, Ord)]
|
||||
pub struct TypedDictType<'db> {
|
||||
/// A reference to the class (inheriting from `typing.TypedDict`) that specifies the
|
||||
/// schema of this `TypedDict`.
|
||||
defining_class: ClassType<'db>,
|
||||
}
|
||||
|
||||
// The Salsa heap is tracked separately.
|
||||
impl get_size2::GetSize for TypedDictType<'_> {}
|
||||
|
||||
impl<'db> TypedDictType<'db> {
|
||||
pub(crate) fn apply_type_mapping<'a>(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
type_mapping: &TypeMapping<'a, 'db>,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
db,
|
||||
self.defining_class(db).apply_type_mapping(db, type_mapping),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn walk_typed_dict_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
|
||||
db: &'db dyn Db,
|
||||
typed_dict: TypedDictType<'db>,
|
||||
visitor: &mut V,
|
||||
) {
|
||||
visitor.visit_type(db, typed_dict.defining_class(db).into());
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum BoundSuperError<'db> {
|
||||
InvalidPivotClassType {
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ use crate::types::signatures::{CallableSignature, Parameter, Parameters, Signatu
|
|||
use crate::types::tuple::TupleSpec;
|
||||
use crate::types::{
|
||||
BareTypeAliasType, Binding, BoundSuperError, BoundSuperType, CallableType, DataclassParams,
|
||||
DeprecatedInstance, DynamicType, KnownInstanceType, TypeAliasType, TypeMapping, TypeRelation,
|
||||
DeprecatedInstance, KnownInstanceType, TypeAliasType, TypeMapping, TypeRelation,
|
||||
TypeTransformer, TypeVarBoundOrConstraints, TypeVarInstance, TypeVarKind, declaration_type,
|
||||
infer_definition_types,
|
||||
infer_definition_types, todo_type,
|
||||
};
|
||||
use crate::{
|
||||
Db, FxIndexMap, FxOrderSet, Program,
|
||||
|
|
@ -112,6 +112,21 @@ fn try_mro_cycle_initial<'db>(
|
|||
))
|
||||
}
|
||||
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
fn is_typed_dict_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &bool,
|
||||
_count: u32,
|
||||
_self: ClassLiteral<'db>,
|
||||
) -> salsa::CycleRecoveryAction<bool> {
|
||||
salsa::CycleRecoveryAction::Iterate
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn is_typed_dict_cycle_initial<'db>(_db: &'db dyn Db, _self: ClassLiteral<'db>) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn try_metaclass_cycle_recover<'db>(
|
||||
_db: &'db dyn Db,
|
||||
_value: &Result<(Type<'db>, Option<DataclassTransformerParams>), MetaclassError<'db>>,
|
||||
|
|
@ -140,6 +155,8 @@ pub(crate) enum CodeGeneratorKind {
|
|||
DataclassLike,
|
||||
/// Classes inheriting from `typing.NamedTuple`
|
||||
NamedTuple,
|
||||
/// Classes inheriting from `typing.TypedDict`
|
||||
TypedDict,
|
||||
}
|
||||
|
||||
impl CodeGeneratorKind {
|
||||
|
|
@ -148,6 +165,8 @@ impl CodeGeneratorKind {
|
|||
Some(CodeGeneratorKind::DataclassLike)
|
||||
} else if CodeGeneratorKind::NamedTuple.matches(db, class) {
|
||||
Some(CodeGeneratorKind::NamedTuple)
|
||||
} else if CodeGeneratorKind::TypedDict.matches(db, class) {
|
||||
Some(CodeGeneratorKind::TypedDict)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
@ -165,6 +184,7 @@ impl CodeGeneratorKind {
|
|||
base.into_class_literal()
|
||||
.is_some_and(|c| c.is_known(db, KnownClass::NamedTuple))
|
||||
}),
|
||||
Self::TypedDict => class.is_typed_dict(db),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -238,6 +258,10 @@ impl<'db> GenericAlias<'db> {
|
|||
// look in `self.tuple`.
|
||||
self.specialization(db).find_legacy_typevars(db, typevars);
|
||||
}
|
||||
|
||||
pub(super) fn is_typed_dict(self, db: &'db dyn Db) -> bool {
|
||||
self.origin(db).is_typed_dict(db)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<GenericAlias<'db>> for Type<'db> {
|
||||
|
|
@ -423,15 +447,6 @@ impl<'db> ClassType<'db> {
|
|||
other: Self,
|
||||
relation: TypeRelation,
|
||||
) -> bool {
|
||||
// TODO: remove this branch once we have proper support for TypedDicts.
|
||||
if self.is_known(db, KnownClass::Dict)
|
||||
&& other
|
||||
.iter_mro(db)
|
||||
.any(|b| matches!(b, ClassBase::Dynamic(DynamicType::TodoTypedDict)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
self.iter_mro(db).any(|base| {
|
||||
match base {
|
||||
ClassBase::Dynamic(_) => match relation {
|
||||
|
|
@ -455,6 +470,11 @@ impl<'db> ClassType<'db> {
|
|||
(ClassType::Generic(_), ClassType::NonGeneric(_))
|
||||
| (ClassType::NonGeneric(_), ClassType::Generic(_)) => false,
|
||||
},
|
||||
|
||||
ClassBase::TypedDict => {
|
||||
// TODO: Implement subclassing and assignability for TypedDicts.
|
||||
true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -873,6 +893,11 @@ impl<'db> ClassType<'db> {
|
|||
/// See [`Type::instance_member`] for more details.
|
||||
pub(super) fn instance_member(self, db: &'db dyn Db, name: &str) -> PlaceAndQualifiers<'db> {
|
||||
let (class_literal, specialization) = self.class_literal(db);
|
||||
|
||||
if class_literal.is_typed_dict(db) {
|
||||
return Place::Unbound.into();
|
||||
}
|
||||
|
||||
class_literal
|
||||
.instance_member(db, specialization, name)
|
||||
.map_type(|ty| ty.apply_optional_specialization(db, specialization))
|
||||
|
|
@ -1457,6 +1482,22 @@ impl<'db> ClassLiteral<'db> {
|
|||
.contains(&ClassBase::Class(other))
|
||||
}
|
||||
|
||||
/// Return `true` if this class constitutes a typed dict specification (inherits from
|
||||
/// `typing.TypedDict`, either directly or indirectly).
|
||||
#[salsa::tracked(
|
||||
cycle_fn=is_typed_dict_cycle_recover,
|
||||
cycle_initial=is_typed_dict_cycle_initial,
|
||||
heap_size=get_size2::heap_size
|
||||
)]
|
||||
pub(super) fn is_typed_dict(self, db: &'db dyn Db) -> bool {
|
||||
if let Some(known) = self.known(db) {
|
||||
return known.is_typed_dict_subclass();
|
||||
}
|
||||
|
||||
self.iter_mro(db, None)
|
||||
.any(|base| matches!(base, ClassBase::TypedDict))
|
||||
}
|
||||
|
||||
/// Return the explicit `metaclass` of this class, if one is defined.
|
||||
///
|
||||
/// ## Note
|
||||
|
|
@ -1692,6 +1733,12 @@ impl<'db> ClassLiteral<'db> {
|
|||
)
|
||||
});
|
||||
}
|
||||
ClassBase::TypedDict => {
|
||||
return KnownClass::TypedDictFallback
|
||||
.to_class_literal(db)
|
||||
.find_name_in_mro_with_policy(db, name, policy)
|
||||
.expect("Will return Some() when called on class literal");
|
||||
}
|
||||
}
|
||||
if lookup_result.is_ok() {
|
||||
break;
|
||||
|
|
@ -1792,7 +1839,7 @@ impl<'db> ClassLiteral<'db> {
|
|||
|
||||
/// Returns the type of a synthesized dataclass member like `__init__` or `__lt__`, or
|
||||
/// a synthesized `__new__` method for a `NamedTuple`.
|
||||
fn own_synthesized_member(
|
||||
pub(super) fn own_synthesized_member(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
|
|
@ -1976,10 +2023,118 @@ impl<'db> ClassLiteral<'db> {
|
|||
}
|
||||
None
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "__setitem__") => {
|
||||
// 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::positional_only(Some(Name::new_static("key"))),
|
||||
Parameter::positional_only(Some(Name::new_static("value"))),
|
||||
]),
|
||||
Some(Type::none(db)),
|
||||
);
|
||||
|
||||
Some(CallableType::function_like(db, signature))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "__getitem__") => {
|
||||
// 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::positional_only(Some(Name::new_static("key"))),
|
||||
]),
|
||||
Some(todo_type!("Support for `TypedDict`")),
|
||||
);
|
||||
|
||||
Some(CallableType::function_like(db, signature))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "get") => {
|
||||
// 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::positional_only(Some(Name::new_static("key"))),
|
||||
Parameter::positional_only(Some(Name::new_static("default")))
|
||||
.with_default_type(Type::unknown()),
|
||||
]),
|
||||
Some(todo_type!("Support for `TypedDict`")),
|
||||
);
|
||||
|
||||
Some(CallableType::function_like(db, signature))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "pop") => {
|
||||
// TODO: synthesize a set of overloads with precise types.
|
||||
// Required keys should be forbidden to be popped.
|
||||
let signature = 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"))),
|
||||
Parameter::positional_only(Some(Name::new_static("default")))
|
||||
.with_default_type(Type::unknown()),
|
||||
]),
|
||||
Some(todo_type!("Support for `TypedDict`")),
|
||||
);
|
||||
|
||||
Some(CallableType::function_like(db, signature))
|
||||
}
|
||||
(CodeGeneratorKind::TypedDict, "setdefault") => {
|
||||
// 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::positional_only(Some(Name::new_static("key"))),
|
||||
Parameter::positional_only(Some(Name::new_static("default"))),
|
||||
]),
|
||||
Some(todo_type!("Support for `TypedDict`")),
|
||||
);
|
||||
|
||||
Some(CallableType::function_like(db, signature))
|
||||
}
|
||||
(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,
|
||||
}
|
||||
}
|
||||
|
||||
/// Member lookup for classes that inherit from `typing.TypedDict`.
|
||||
///
|
||||
/// This is implemented as a separate method because the item definitions on a `TypedDict`-based
|
||||
/// class are *not* accessible as class members. Instead, this mostly defers to `TypedDictFallback`,
|
||||
/// unless `name` corresponds to one of the specialized synthetic members like `__getitem__`.
|
||||
pub(crate) fn typed_dict_member(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
specialization: Option<Specialization<'db>>,
|
||||
name: &str,
|
||||
policy: MemberLookupPolicy,
|
||||
) -> PlaceAndQualifiers<'db> {
|
||||
if let Some(member) = self.own_synthesized_member(db, specialization, name) {
|
||||
Place::bound(member).into()
|
||||
} else {
|
||||
KnownClass::TypedDictFallback
|
||||
.to_class_literal(db)
|
||||
.find_name_in_mro_with_policy(db, name, policy)
|
||||
.expect("`find_name_in_mro_with_policy` will return `Some()` when called on class literal")
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a list of all annotated attributes defined in this class, or any of its superclasses.
|
||||
///
|
||||
/// See [`ClassLiteral::own_fields`] for more details.
|
||||
|
|
@ -2110,6 +2265,10 @@ impl<'db> ClassLiteral<'db> {
|
|||
specialization: Option<Specialization<'db>>,
|
||||
name: &str,
|
||||
) -> PlaceAndQualifiers<'db> {
|
||||
if self.is_typed_dict(db) {
|
||||
return Place::Unbound.into();
|
||||
}
|
||||
|
||||
let mut union = UnionBuilder::new(db);
|
||||
let mut union_qualifiers = TypeQualifiers::empty();
|
||||
|
||||
|
|
@ -2147,6 +2306,9 @@ impl<'db> ClassLiteral<'db> {
|
|||
union = union.add(ty);
|
||||
}
|
||||
}
|
||||
ClassBase::TypedDict => {
|
||||
return Place::bound(todo_type!("Support for `TypedDict`")).into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2862,6 +3024,7 @@ pub enum KnownClass {
|
|||
InitVar,
|
||||
// _typeshed._type_checker_internals
|
||||
NamedTupleFallback,
|
||||
TypedDictFallback,
|
||||
}
|
||||
|
||||
impl KnownClass {
|
||||
|
|
@ -2958,7 +3121,8 @@ impl KnownClass {
|
|||
| Self::Field
|
||||
| Self::KwOnly
|
||||
| Self::InitVar
|
||||
| Self::NamedTupleFallback => Truthiness::Ambiguous,
|
||||
| Self::NamedTupleFallback
|
||||
| Self::TypedDictFallback => Truthiness::Ambiguous,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3040,6 +3204,7 @@ impl KnownClass {
|
|||
| Self::SupportsIndex
|
||||
| Self::NamedTuple
|
||||
| Self::NamedTupleFallback
|
||||
| Self::TypedDictFallback
|
||||
| Self::Counter
|
||||
| Self::DefaultDict
|
||||
| Self::OrderedDict
|
||||
|
|
@ -3123,7 +3288,85 @@ impl KnownClass {
|
|||
| KnownClass::Field
|
||||
| KnownClass::KwOnly
|
||||
| KnownClass::InitVar
|
||||
| KnownClass::NamedTupleFallback => false,
|
||||
| KnownClass::NamedTupleFallback
|
||||
| KnownClass::TypedDictFallback => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if this class is a (true) subclass of `typing.TypedDict`.
|
||||
pub(crate) const fn is_typed_dict_subclass(self) -> bool {
|
||||
match self {
|
||||
KnownClass::Bool
|
||||
| KnownClass::Object
|
||||
| KnownClass::Bytes
|
||||
| KnownClass::Bytearray
|
||||
| KnownClass::Type
|
||||
| KnownClass::Int
|
||||
| KnownClass::Float
|
||||
| KnownClass::Complex
|
||||
| KnownClass::Str
|
||||
| KnownClass::List
|
||||
| KnownClass::Tuple
|
||||
| KnownClass::Set
|
||||
| KnownClass::FrozenSet
|
||||
| KnownClass::Dict
|
||||
| KnownClass::Slice
|
||||
| KnownClass::Property
|
||||
| KnownClass::BaseException
|
||||
| KnownClass::Exception
|
||||
| KnownClass::BaseExceptionGroup
|
||||
| KnownClass::ExceptionGroup
|
||||
| KnownClass::Staticmethod
|
||||
| KnownClass::Classmethod
|
||||
| KnownClass::Awaitable
|
||||
| KnownClass::Generator
|
||||
| KnownClass::Deprecated
|
||||
| KnownClass::Super
|
||||
| KnownClass::Enum
|
||||
| KnownClass::EnumType
|
||||
| KnownClass::Auto
|
||||
| KnownClass::Member
|
||||
| KnownClass::Nonmember
|
||||
| KnownClass::ABCMeta
|
||||
| KnownClass::GenericAlias
|
||||
| KnownClass::ModuleType
|
||||
| KnownClass::FunctionType
|
||||
| KnownClass::MethodType
|
||||
| KnownClass::MethodWrapperType
|
||||
| KnownClass::WrapperDescriptorType
|
||||
| KnownClass::UnionType
|
||||
| KnownClass::GeneratorType
|
||||
| KnownClass::AsyncGeneratorType
|
||||
| KnownClass::CoroutineType
|
||||
| KnownClass::NoneType
|
||||
| KnownClass::Any
|
||||
| KnownClass::StdlibAlias
|
||||
| KnownClass::SpecialForm
|
||||
| KnownClass::TypeVar
|
||||
| KnownClass::ParamSpec
|
||||
| KnownClass::ParamSpecArgs
|
||||
| KnownClass::ParamSpecKwargs
|
||||
| KnownClass::TypeVarTuple
|
||||
| KnownClass::TypeAliasType
|
||||
| KnownClass::NoDefaultType
|
||||
| KnownClass::NamedTuple
|
||||
| KnownClass::NewType
|
||||
| KnownClass::SupportsIndex
|
||||
| KnownClass::Iterable
|
||||
| KnownClass::Iterator
|
||||
| KnownClass::ChainMap
|
||||
| KnownClass::Counter
|
||||
| KnownClass::DefaultDict
|
||||
| KnownClass::Deque
|
||||
| KnownClass::OrderedDict
|
||||
| KnownClass::VersionInfo
|
||||
| KnownClass::EllipsisType
|
||||
| KnownClass::NotImplementedType
|
||||
| KnownClass::Field
|
||||
| KnownClass::KwOnly
|
||||
| KnownClass::InitVar
|
||||
| KnownClass::NamedTupleFallback
|
||||
| KnownClass::TypedDictFallback => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3211,7 +3454,8 @@ impl KnownClass {
|
|||
| Self::Field
|
||||
| Self::KwOnly
|
||||
| Self::InitVar
|
||||
| Self::NamedTupleFallback => false,
|
||||
| Self::NamedTupleFallback
|
||||
| Self::TypedDictFallback => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3307,6 +3551,7 @@ impl KnownClass {
|
|||
Self::KwOnly => "KW_ONLY",
|
||||
Self::InitVar => "InitVar",
|
||||
Self::NamedTupleFallback => "NamedTupleFallback",
|
||||
Self::TypedDictFallback => "TypedDictFallback",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3563,7 +3808,7 @@ impl KnownClass {
|
|||
| Self::Deque
|
||||
| Self::OrderedDict => KnownModule::Collections,
|
||||
Self::Field | Self::KwOnly | Self::InitVar => KnownModule::Dataclasses,
|
||||
Self::NamedTupleFallback => KnownModule::TypeCheckerInternals,
|
||||
Self::NamedTupleFallback | Self::TypedDictFallback => KnownModule::TypeCheckerInternals,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3640,7 +3885,8 @@ impl KnownClass {
|
|||
| Self::InitVar
|
||||
| Self::Iterable
|
||||
| Self::Iterator
|
||||
| Self::NamedTupleFallback => false,
|
||||
| Self::NamedTupleFallback
|
||||
| Self::TypedDictFallback => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3719,7 +3965,8 @@ impl KnownClass {
|
|||
| Self::InitVar
|
||||
| Self::Iterable
|
||||
| Self::Iterator
|
||||
| Self::NamedTupleFallback => false,
|
||||
| Self::NamedTupleFallback
|
||||
| Self::TypedDictFallback => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3809,6 +4056,7 @@ impl KnownClass {
|
|||
"KW_ONLY" => Self::KwOnly,
|
||||
"InitVar" => Self::InitVar,
|
||||
"NamedTupleFallback" => Self::NamedTupleFallback,
|
||||
"TypedDictFallback" => Self::TypedDictFallback,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
|
|
@ -3873,6 +4121,7 @@ impl KnownClass {
|
|||
| Self::KwOnly
|
||||
| Self::InitVar
|
||||
| Self::NamedTupleFallback
|
||||
| Self::TypedDictFallback
|
||||
| Self::Awaitable
|
||||
| Self::Generator => module == self.canonical_module(db),
|
||||
Self::NoneType => matches!(module, KnownModule::Typeshed | KnownModule::Types),
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ pub enum ClassBase<'db> {
|
|||
/// but nonetheless appears in the MRO of classes that inherit from `Generic[T]`,
|
||||
/// `Protocol[T]`, or bare `Protocol`.
|
||||
Generic,
|
||||
TypedDict,
|
||||
}
|
||||
|
||||
impl<'db> ClassBase<'db> {
|
||||
|
|
@ -39,7 +40,7 @@ impl<'db> ClassBase<'db> {
|
|||
match self {
|
||||
Self::Dynamic(dynamic) => Self::Dynamic(dynamic.normalized()),
|
||||
Self::Class(class) => Self::Class(class.normalized_impl(db, visitor)),
|
||||
Self::Protocol | Self::Generic => self,
|
||||
Self::Protocol | Self::Generic | Self::TypedDict => self,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,11 +52,11 @@ impl<'db> ClassBase<'db> {
|
|||
ClassBase::Dynamic(
|
||||
DynamicType::Todo(_)
|
||||
| DynamicType::TodoPEP695ParamSpec
|
||||
| DynamicType::TodoTypeAlias
|
||||
| DynamicType::TodoTypedDict,
|
||||
| DynamicType::TodoTypeAlias,
|
||||
) => "@Todo",
|
||||
ClassBase::Protocol => "Protocol",
|
||||
ClassBase::Generic => "Generic",
|
||||
ClassBase::TypedDict => "TypedDict",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +159,8 @@ impl<'db> ClassBase<'db> {
|
|||
| Type::ProtocolInstance(_)
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy
|
||||
| Type::TypeIs(_) => None,
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_) => None,
|
||||
|
||||
Type::KnownInstance(known_instance) => match known_instance {
|
||||
KnownInstanceType::SubscriptedGeneric(_) => Some(Self::Generic),
|
||||
|
|
@ -234,7 +236,7 @@ impl<'db> ClassBase<'db> {
|
|||
SpecialFormType::OrderedDict => {
|
||||
Self::try_from_type(db, KnownClass::OrderedDict.to_class_literal(db))
|
||||
}
|
||||
SpecialFormType::TypedDict => Some(Self::Dynamic(DynamicType::TodoTypedDict)),
|
||||
SpecialFormType::TypedDict => Some(Self::TypedDict),
|
||||
SpecialFormType::Callable => {
|
||||
Self::try_from_type(db, todo_type!("Support for Callable as a base class"))
|
||||
}
|
||||
|
|
@ -245,14 +247,14 @@ impl<'db> ClassBase<'db> {
|
|||
pub(super) fn into_class(self) -> Option<ClassType<'db>> {
|
||||
match self {
|
||||
Self::Class(class) => Some(class),
|
||||
Self::Dynamic(_) | Self::Generic | Self::Protocol => None,
|
||||
Self::Dynamic(_) | Self::Generic | Self::Protocol | Self::TypedDict => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_type_mapping<'a>(self, db: &'db dyn Db, type_mapping: &TypeMapping<'a, 'db>) -> Self {
|
||||
match self {
|
||||
Self::Class(class) => Self::Class(class.apply_type_mapping(db, type_mapping)),
|
||||
Self::Dynamic(_) | Self::Generic | Self::Protocol => self,
|
||||
Self::Dynamic(_) | Self::Generic | Self::Protocol | Self::TypedDict => self,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -276,7 +278,10 @@ impl<'db> ClassBase<'db> {
|
|||
.try_mro(db, specialization)
|
||||
.is_err_and(MroError::is_cycle)
|
||||
}
|
||||
ClassBase::Dynamic(_) | ClassBase::Generic | ClassBase::Protocol => false,
|
||||
ClassBase::Dynamic(_)
|
||||
| ClassBase::Generic
|
||||
| ClassBase::Protocol
|
||||
| ClassBase::TypedDict => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -288,7 +293,9 @@ impl<'db> ClassBase<'db> {
|
|||
) -> impl Iterator<Item = ClassBase<'db>> {
|
||||
match self {
|
||||
ClassBase::Protocol => ClassBaseMroIterator::length_3(db, self, ClassBase::Generic),
|
||||
ClassBase::Dynamic(_) | ClassBase::Generic => ClassBaseMroIterator::length_2(db, self),
|
||||
ClassBase::Dynamic(_) | ClassBase::Generic | ClassBase::TypedDict => {
|
||||
ClassBaseMroIterator::length_2(db, self)
|
||||
}
|
||||
ClassBase::Class(class) => {
|
||||
ClassBaseMroIterator::from_class(db, class, additional_specialization)
|
||||
}
|
||||
|
|
@ -309,6 +316,7 @@ impl<'db> From<ClassBase<'db>> for Type<'db> {
|
|||
ClassBase::Class(class) => class.into(),
|
||||
ClassBase::Protocol => Type::SpecialForm(SpecialFormType::Protocol),
|
||||
ClassBase::Generic => Type::SpecialForm(SpecialFormType::Generic),
|
||||
ClassBase::TypedDict => Type::SpecialForm(SpecialFormType::TypedDict),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -234,6 +234,9 @@ impl Display for DisplayRepresentation<'_> {
|
|||
}
|
||||
f.write_str("]")
|
||||
}
|
||||
Type::TypedDict(typed_dict) => {
|
||||
f.write_str(typed_dict.defining_class(self.db).name(self.db))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -975,7 +975,8 @@ fn is_instance_truthiness<'db>(
|
|||
| Type::TypeIs(..)
|
||||
| Type::Callable(..)
|
||||
| Type::Dynamic(..)
|
||||
| Type::Never => {
|
||||
| Type::Never
|
||||
| Type::TypedDict(_) => {
|
||||
// We could probably try to infer more precise types in some of these cases, but it's unclear
|
||||
// if it's worth the effort.
|
||||
Truthiness::Ambiguous
|
||||
|
|
|
|||
|
|
@ -98,6 +98,14 @@ impl<'db> AllMembers<'db> {
|
|||
self.extend_with_instance_members(db, class_literal);
|
||||
}
|
||||
|
||||
Type::ClassLiteral(class_literal) if class_literal.is_typed_dict(db) => {
|
||||
self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db));
|
||||
}
|
||||
|
||||
Type::GenericAlias(generic_alias) if generic_alias.is_typed_dict(db) => {
|
||||
self.extend_with_type(db, KnownClass::TypedDictFallback.to_class_literal(db));
|
||||
}
|
||||
|
||||
Type::ClassLiteral(class_literal) => {
|
||||
self.extend_with_class_members(db, ty, class_literal);
|
||||
|
||||
|
|
@ -155,6 +163,14 @@ impl<'db> AllMembers<'db> {
|
|||
_ => {}
|
||||
},
|
||||
|
||||
Type::TypedDict(_) => {
|
||||
if let Type::ClassLiteral(class_literal) = ty.to_meta_type(db) {
|
||||
self.extend_with_class_members(db, ty, class_literal);
|
||||
}
|
||||
|
||||
self.extend_with_type(db, KnownClass::TypedDictFallback.to_instance(db));
|
||||
}
|
||||
|
||||
Type::ModuleLiteral(literal) => {
|
||||
self.extend_with_type(db, KnownClass::ModuleType.to_instance(db));
|
||||
let module = literal.module(db);
|
||||
|
|
|
|||
|
|
@ -3906,7 +3906,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
| Type::TypeVar(..)
|
||||
| Type::AlwaysTruthy
|
||||
| Type::AlwaysFalsy
|
||||
| Type::TypeIs(_) => {
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_) => {
|
||||
// First, try to call the `__setattr__` dunder method. If this is present/defined, overrides
|
||||
// assigning the attributed by the normal mechanism.
|
||||
let setattr_dunder_call_result = object_ty.try_call_dunder_with_policy(
|
||||
|
|
@ -7149,7 +7150,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
| Type::Tuple(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypeVar(_)
|
||||
| Type::TypeIs(_),
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_),
|
||||
) => {
|
||||
let unary_dunder_method = match op {
|
||||
ast::UnaryOp::Invert => "__invert__",
|
||||
|
|
@ -7274,8 +7276,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
todo @ Type::Dynamic(
|
||||
DynamicType::Todo(_)
|
||||
| DynamicType::TodoPEP695ParamSpec
|
||||
| DynamicType::TodoTypeAlias
|
||||
| DynamicType::TodoTypedDict,
|
||||
| DynamicType::TodoTypeAlias,
|
||||
),
|
||||
_,
|
||||
_,
|
||||
|
|
@ -7285,8 +7286,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
todo @ Type::Dynamic(
|
||||
DynamicType::Todo(_)
|
||||
| DynamicType::TodoPEP695ParamSpec
|
||||
| DynamicType::TodoTypeAlias
|
||||
| DynamicType::TodoTypedDict,
|
||||
| DynamicType::TodoTypeAlias,
|
||||
),
|
||||
_,
|
||||
) => Some(todo),
|
||||
|
|
@ -7474,7 +7474,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
| Type::Tuple(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypeVar(_)
|
||||
| Type::TypeIs(_),
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_),
|
||||
Type::FunctionLiteral(_)
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::Callable(..)
|
||||
|
|
@ -7503,7 +7504,8 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
| Type::Tuple(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypeVar(_)
|
||||
| Type::TypeIs(_),
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypedDict(_),
|
||||
op,
|
||||
) => {
|
||||
// We either want to call lhs.__op__ or rhs.__rop__. The full decision tree from
|
||||
|
|
|
|||
|
|
@ -269,7 +269,10 @@ impl<'db> Mro<'db> {
|
|||
continue;
|
||||
}
|
||||
match base {
|
||||
ClassBase::Class(_) | ClassBase::Generic | ClassBase::Protocol => {
|
||||
ClassBase::Class(_)
|
||||
| ClassBase::Generic
|
||||
| ClassBase::Protocol
|
||||
| ClassBase::TypedDict => {
|
||||
errors.push(DuplicateBaseError {
|
||||
duplicate_base: base,
|
||||
first_index: *first_index,
|
||||
|
|
|
|||
|
|
@ -256,7 +256,8 @@ impl ClassInfoConstraintFunction {
|
|||
| Type::KnownInstance(_)
|
||||
| Type::TypeIs(_)
|
||||
| Type::WrapperDescriptor(_)
|
||||
| Type::DataclassTransformer(_) => None,
|
||||
| Type::DataclassTransformer(_)
|
||||
| Type::TypedDict(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,6 +166,9 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
|
|||
(ClassBase::Generic, _) => Ordering::Less,
|
||||
(_, ClassBase::Generic) => Ordering::Greater,
|
||||
|
||||
(ClassBase::TypedDict, _) => Ordering::Less,
|
||||
(_, ClassBase::TypedDict) => Ordering::Greater,
|
||||
|
||||
(ClassBase::Dynamic(left), ClassBase::Dynamic(right)) => {
|
||||
dynamic_elements_ordering(left, right)
|
||||
}
|
||||
|
|
@ -234,6 +237,10 @@ pub(super) fn union_or_intersection_elements_ordering<'db>(
|
|||
|
||||
unreachable!("Two equal, normalized intersections should share the same Salsa ID")
|
||||
}
|
||||
|
||||
(Type::TypedDict(left), Type::TypedDict(right)) => left.cmp(right),
|
||||
(Type::TypedDict(_), _) => Ordering::Less,
|
||||
(_, Type::TypedDict(_)) => Ordering::Greater,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -257,9 +264,6 @@ fn dynamic_elements_ordering(left: DynamicType, right: DynamicType) -> Ordering
|
|||
|
||||
(DynamicType::TodoTypeAlias, _) => Ordering::Less,
|
||||
(_, DynamicType::TodoTypeAlias) => Ordering::Greater,
|
||||
|
||||
(DynamicType::TodoTypedDict, _) => Ordering::Less,
|
||||
(_, DynamicType::TodoTypedDict) => Ordering::Greater,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use crate::{
|
|||
BoundMethodType, BoundSuperType, CallableType, GenericAlias, IntersectionType,
|
||||
KnownInstanceType, MethodWrapperKind, NominalInstanceType, PropertyInstanceType,
|
||||
ProtocolInstanceType, SubclassOfType, Type, TypeAliasType, TypeIsType, TypeVarInstance,
|
||||
UnionType,
|
||||
TypedDictType, UnionType,
|
||||
class::walk_generic_alias,
|
||||
function::{FunctionType, walk_function_type},
|
||||
instance::{walk_nominal_instance_type, walk_protocol_instance_type},
|
||||
|
|
@ -12,7 +12,8 @@ use crate::{
|
|||
tuple::{TupleType, walk_tuple_type},
|
||||
walk_bound_method_type, walk_bound_super_type, walk_callable_type, walk_intersection_type,
|
||||
walk_known_instance_type, walk_method_wrapper_type, walk_property_instance_type,
|
||||
walk_type_alias_type, walk_type_var_type, walk_typeis_type, walk_union,
|
||||
walk_type_alias_type, walk_type_var_type, walk_typed_dict_type, walk_typeis_type,
|
||||
walk_union,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -107,6 +108,10 @@ pub(crate) trait TypeVisitor<'db> {
|
|||
fn visit_type_alias_type(&mut self, db: &'db dyn Db, type_alias: TypeAliasType<'db>) {
|
||||
walk_type_alias_type(db, type_alias, self);
|
||||
}
|
||||
|
||||
fn visit_typed_dict_type(&mut self, db: &'db dyn Db, typed_dict: TypedDictType<'db>) {
|
||||
walk_typed_dict_type(db, typed_dict, self);
|
||||
}
|
||||
}
|
||||
|
||||
/// Enumeration of types that may contain other types, such as unions, intersections, and generics.
|
||||
|
|
@ -128,6 +133,7 @@ enum NonAtomicType<'db> {
|
|||
TypeIs(TypeIsType<'db>),
|
||||
TypeVar(TypeVarInstance<'db>),
|
||||
ProtocolInstance(ProtocolInstanceType<'db>),
|
||||
TypedDict(TypedDictType<'db>),
|
||||
}
|
||||
|
||||
enum TypeKind<'db> {
|
||||
|
|
@ -190,6 +196,9 @@ impl<'db> From<Type<'db>> for TypeKind<'db> {
|
|||
}
|
||||
Type::TypeVar(type_var) => TypeKind::NonAtomic(NonAtomicType::TypeVar(type_var)),
|
||||
Type::TypeIs(type_is) => TypeKind::NonAtomic(NonAtomicType::TypeIs(type_is)),
|
||||
Type::TypedDict(typed_dict) => {
|
||||
TypeKind::NonAtomic(NonAtomicType::TypedDict(typed_dict))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -226,6 +235,7 @@ fn walk_non_atomic_type<'db, V: TypeVisitor<'db> + ?Sized>(
|
|||
NonAtomicType::ProtocolInstance(protocol) => {
|
||||
visitor.visit_protocol_instance_type(db, protocol);
|
||||
}
|
||||
NonAtomicType::TypedDict(typed_dict) => visitor.visit_typed_dict_type(db, typed_dict),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ ty_python_semantic = { path = "../crates/ty_python_semantic" }
|
|||
ty_vendored = { path = "../crates/ty_vendored" }
|
||||
|
||||
libfuzzer-sys = { git = "https://github.com/rust-fuzz/libfuzzer", default-features = false }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "86ca4a9d70e97dd5107e6111a09647dd10ff7535", default-features = false, features = [
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "d66fe331d546216132ace503512b94d5c68d2c50", default-features = false, features = [
|
||||
"compact_str",
|
||||
"macros",
|
||||
"salsa_unstable",
|
||||
|
|
|
|||
Loading…
Reference in New Issue