Skip to content

Commit 54635de

Browse files
authored
[dataclass_transform] include __dataclass_fields__ in transformed types (#14752)
`dataclasses` uses a `__dataclass_fields__` attribute on each class to mark that it is a dataclass, and Typeshed checks for this attribute in its stubs for functions like `dataclasses.is_dataclass` and `dataclasses.asdict`. In #14667, I mistakenly removed this attribute for classes transformed by a `dataclass_transform`. This was due to a misinterpretation of PEP 681 on my part; after rereading the [section on dataclass semantics](https://peps.python.org/pep-0681/#dataclass-semantics), it says: > Except where stated otherwise in this PEP, classes impacted by `dataclass_transform`, either by inheriting from a class that is decorated with `dataclass_transform` or by being decorated with a function decorated with `dataclass_transform`, are assumed to behave like stdlib dataclass. The PEP doesn't seem to state anything about `__dataclass_fields__` or the related functions as far as I can tell, so we should assume that transforms should match the behavior of `dataclasses.dataclass` in this regard and include the attribute. This also matches the behavior of Pyright, which the PEP defines as the reference implementation.
1 parent 29bcc7f commit 54635de

File tree

2 files changed

+11
-11
lines changed

2 files changed

+11
-11
lines changed

mypy/plugins/dataclasses.py

+10-9
Original file line numberDiff line numberDiff line change
@@ -648,17 +648,18 @@ def _is_kw_only_type(self, node: Type | None) -> bool:
648648
return node_type.type.fullname == "dataclasses.KW_ONLY"
649649

650650
def _add_dataclass_fields_magic_attribute(self) -> None:
651-
# Only add if the class is a dataclasses dataclass, and omit it for dataclass_transform
652-
# classes.
653-
# It would be nice if this condition were reified rather than using an `is` check.
654-
# Only add if the class is a dataclasses dataclass, and omit it for dataclass_transform
655-
# classes.
656-
if self._spec is not _TRANSFORM_SPEC_FOR_DATACLASSES:
657-
return
658-
659651
attr_name = "__dataclass_fields__"
660652
any_type = AnyType(TypeOfAny.explicit)
661-
field_type = self._api.named_type_or_none("dataclasses.Field", [any_type]) or any_type
653+
# For `dataclasses`, use the type `dict[str, Field[Any]]` for accuracy. For dataclass
654+
# transforms, it's inaccurate to use `Field` since a given transform may use a completely
655+
# different type (or none); fall back to `Any` there.
656+
#
657+
# In either case, we're aiming to match the Typeshed stub for `is_dataclass`, which expects
658+
# the instance to have a `__dataclass_fields__` attribute of type `dict[str, Field[Any]]`.
659+
if self._spec is _TRANSFORM_SPEC_FOR_DATACLASSES:
660+
field_type = self._api.named_type_or_none("dataclasses.Field", [any_type]) or any_type
661+
else:
662+
field_type = any_type
662663
attr_type = self._api.named_type(
663664
"builtins.dict", [self._api.named_type("builtins.str"), field_type]
664665
)

test-data/unit/check-dataclass-transform.test

+1-2
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,7 @@ class Bad:
279279
bad1: int = field(alias=some_str()) # E: "alias" argument to dataclass field must be a string literal
280280
bad2: int = field(kw_only=some_bool()) # E: "kw_only" argument must be a boolean literal
281281

282-
# this metadata should only exist for dataclasses.dataclass classes
283-
Foo.__dataclass_fields__ # E: "Type[Foo]" has no attribute "__dataclass_fields__"
282+
reveal_type(Foo.__dataclass_fields__) # N: Revealed type is "builtins.dict[builtins.str, Any]"
284283

285284
[typing fixtures/typing-full.pyi]
286285
[builtins fixtures/dataclasses.pyi]

0 commit comments

Comments
 (0)