Skip to content

Commit 279dac1

Browse files
authored
[ty] Make dataclass instances adhere to DataclassInstance (#18115)
## Summary Make dataclass instances adhere to the `DataclassInstance` protocol. fixes astral-sh/ty#400 ## Test Plan New Markdown tests
1 parent 5761703 commit 279dac1

File tree

3 files changed

+40
-19
lines changed

3 files changed

+40
-19
lines changed

crates/ty_python_semantic/resources/mdtest/dataclasses.md

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -618,23 +618,45 @@ To do
618618

619619
## `dataclass.fields`
620620

621-
Dataclasses have `__dataclass_fields__` in them, which makes them a subtype of the
622-
`DataclassInstance` protocol.
623-
624-
Here, we verify that dataclasses can be passed to `dataclasses.fields` without any errors, and that
625-
the return type of `dataclasses.fields` is correct.
621+
Dataclasses have a special `__dataclass_fields__` class variable member. The `DataclassInstance`
622+
protocol checks for the presence of this attribute. It is used in the `dataclasses.fields` and
623+
`dataclasses.asdict` functions, for example:
626624

627625
```py
628-
from dataclasses import dataclass, fields
626+
from dataclasses import dataclass, fields, asdict
629627

630628
@dataclass
631629
class Foo:
632630
x: int
633631

632+
foo = Foo(1)
633+
634+
reveal_type(foo.__dataclass_fields__) # revealed: dict[str, Field[Any]]
635+
reveal_type(fields(Foo)) # revealed: tuple[Field[Any], ...]
636+
reveal_type(asdict(foo)) # revealed: dict[str, Any]
637+
```
638+
639+
The class objects themselves also have a `__dataclass_fields__` attribute:
640+
641+
```py
634642
reveal_type(Foo.__dataclass_fields__) # revealed: dict[str, Field[Any]]
643+
```
644+
645+
They can be passed into `fields` as well, because it also accepts `type[DataclassInstance]`
646+
arguments:
647+
648+
```py
635649
reveal_type(fields(Foo)) # revealed: tuple[Field[Any], ...]
636650
```
637651

652+
But calling `asdict` on the class object is not allowed:
653+
654+
```py
655+
# TODO: this should be a invalid-argument-type error, but we don't properly check the
656+
# types (and more importantly, the `ClassVar` type qualifier) of protocol members yet.
657+
asdict(Foo)
658+
```
659+
638660
## Other special cases
639661

640662
### `dataclasses.dataclass`

crates/ty_python_semantic/src/types.rs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2957,19 +2957,6 @@ impl<'db> Type<'db> {
29572957
))
29582958
.into()
29592959
}
2960-
Type::ClassLiteral(class)
2961-
if name == "__dataclass_fields__" && class.dataclass_params(db).is_some() =>
2962-
{
2963-
// Make this class look like a subclass of the `DataClassInstance` protocol
2964-
Symbol::bound(KnownClass::Dict.to_specialized_instance(
2965-
db,
2966-
[
2967-
KnownClass::Str.to_instance(db),
2968-
KnownClass::Field.to_specialized_instance(db, [Type::any()]),
2969-
],
2970-
))
2971-
.with_qualifiers(TypeQualifiers::CLASS_VAR)
2972-
}
29732960
Type::BoundMethod(bound_method) => match name_str {
29742961
"__self__" => Symbol::bound(bound_method.self_instance(db)).into(),
29752962
"__func__" => {

crates/ty_python_semantic/src/types/class.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1132,6 +1132,18 @@ impl<'db> ClassLiteral<'db> {
11321132
specialization: Option<Specialization<'db>>,
11331133
name: &str,
11341134
) -> SymbolAndQualifiers<'db> {
1135+
if name == "__dataclass_fields__" && self.dataclass_params(db).is_some() {
1136+
// Make this class look like a subclass of the `DataClassInstance` protocol
1137+
return Symbol::bound(KnownClass::Dict.to_specialized_instance(
1138+
db,
1139+
[
1140+
KnownClass::Str.to_instance(db),
1141+
KnownClass::Field.to_specialized_instance(db, [Type::any()]),
1142+
],
1143+
))
1144+
.with_qualifiers(TypeQualifiers::CLASS_VAR);
1145+
}
1146+
11351147
let body_scope = self.body_scope(db);
11361148
let symbol = class_symbol(db, body_scope, name).map_type(|ty| {
11371149
// The `__new__` and `__init__` members of a non-specialized generic class are handled

0 commit comments

Comments
 (0)