[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:
David Peter 2025-08-05 11:19:49 +02:00 committed by GitHub
parent 351121c5c5
commit 14fbc2b167
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 574 additions and 79 deletions

6
Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -256,7 +256,8 @@ impl ClassInfoConstraintFunction {
| Type::KnownInstance(_)
| Type::TypeIs(_)
| Type::WrapperDescriptor(_)
| Type::DataclassTransformer(_) => None,
| Type::DataclassTransformer(_)
| Type::TypedDict(_) => None,
}
}
}

View File

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

View File

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

View File

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