diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses.md index 7b5377d013..5db68ca81b 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses.md @@ -618,21 +618,43 @@ To do ## `dataclass.fields` -Dataclasses have `__dataclass_fields__` in them, which makes them a subtype of the -`DataclassInstance` protocol. - -Here, we verify that dataclasses can be passed to `dataclasses.fields` without any errors, and that -the return type of `dataclasses.fields` is correct. +Dataclasses have a special `__dataclass_fields__` class variable member. The `DataclassInstance` +protocol checks for the presence of this attribute. It is used in the `dataclasses.fields` and +`dataclasses.asdict` functions, for example: ```py -from dataclasses import dataclass, fields +from dataclasses import dataclass, fields, asdict @dataclass class Foo: x: int -reveal_type(Foo.__dataclass_fields__) # revealed: dict[str, Field[Any]] +foo = Foo(1) + +reveal_type(foo.__dataclass_fields__) # revealed: dict[str, Field[Any]] reveal_type(fields(Foo)) # revealed: tuple[Field[Any], ...] +reveal_type(asdict(foo)) # revealed: dict[str, Any] +``` + +The class objects themselves also have a `__dataclass_fields__` attribute: + +```py +reveal_type(Foo.__dataclass_fields__) # revealed: dict[str, Field[Any]] +``` + +They can be passed into `fields` as well, because it also accepts `type[DataclassInstance]` +arguments: + +```py +reveal_type(fields(Foo)) # revealed: tuple[Field[Any], ...] +``` + +But calling `asdict` on the class object is not allowed: + +```py +# TODO: this should be a invalid-argument-type error, but we don't properly check the +# types (and more importantly, the `ClassVar` type qualifier) of protocol members yet. +asdict(Foo) ``` ## Other special cases diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index ccf6557e8f..77da747c46 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -2957,19 +2957,6 @@ impl<'db> Type<'db> { )) .into() } - Type::ClassLiteral(class) - if name == "__dataclass_fields__" && class.dataclass_params(db).is_some() => - { - // Make this class look like a subclass of the `DataClassInstance` protocol - Symbol::bound(KnownClass::Dict.to_specialized_instance( - db, - [ - KnownClass::Str.to_instance(db), - KnownClass::Field.to_specialized_instance(db, [Type::any()]), - ], - )) - .with_qualifiers(TypeQualifiers::CLASS_VAR) - } Type::BoundMethod(bound_method) => match name_str { "__self__" => Symbol::bound(bound_method.self_instance(db)).into(), "__func__" => { diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 59b6efcd04..d5763dfe62 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1132,6 +1132,18 @@ impl<'db> ClassLiteral<'db> { specialization: Option>, name: &str, ) -> SymbolAndQualifiers<'db> { + if name == "__dataclass_fields__" && self.dataclass_params(db).is_some() { + // Make this class look like a subclass of the `DataClassInstance` protocol + return Symbol::bound(KnownClass::Dict.to_specialized_instance( + db, + [ + KnownClass::Str.to_instance(db), + KnownClass::Field.to_specialized_instance(db, [Type::any()]), + ], + )) + .with_qualifiers(TypeQualifiers::CLASS_VAR); + } + let body_scope = self.body_scope(db); let symbol = class_symbol(db, body_scope, name).map_type(|ty| { // The `__new__` and `__init__` members of a non-specialized generic class are handled