Skip to content

Commit

Permalink
[1.1 backport] [dataclass_transform] include __dataclass_fields__ in …
Browse files Browse the repository at this point in the history
…transformed types (#14752) (#14769)

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

(cherry picked from commit 54635de)

Co-authored-by: Wesley Collin Wright <[email protected]>
  • Loading branch information
cdce8p and wesleywright authored Feb 23, 2023
1 parent f2cac4a commit 17fba49
Show file tree
Hide file tree
Showing 2 changed files with 11 additions and 11 deletions.
19 changes: 10 additions & 9 deletions mypy/plugins/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,17 +648,18 @@ def _is_kw_only_type(self, node: Type | None) -> bool:
return node_type.type.fullname == "dataclasses.KW_ONLY"

def _add_dataclass_fields_magic_attribute(self) -> None:
# Only add if the class is a dataclasses dataclass, and omit it for dataclass_transform
# classes.
# It would be nice if this condition were reified rather than using an `is` check.
# Only add if the class is a dataclasses dataclass, and omit it for dataclass_transform
# classes.
if self._spec is not _TRANSFORM_SPEC_FOR_DATACLASSES:
return

attr_name = "__dataclass_fields__"
any_type = AnyType(TypeOfAny.explicit)
field_type = self._api.named_type_or_none("dataclasses.Field", [any_type]) or any_type
# For `dataclasses`, use the type `dict[str, Field[Any]]` for accuracy. For dataclass
# transforms, it's inaccurate to use `Field` since a given transform may use a completely
# different type (or none); fall back to `Any` there.
#
# In either case, we're aiming to match the Typeshed stub for `is_dataclass`, which expects
# the instance to have a `__dataclass_fields__` attribute of type `dict[str, Field[Any]]`.
if self._spec is _TRANSFORM_SPEC_FOR_DATACLASSES:
field_type = self._api.named_type_or_none("dataclasses.Field", [any_type]) or any_type
else:
field_type = any_type
attr_type = self._api.named_type(
"builtins.dict", [self._api.named_type("builtins.str"), field_type]
)
Expand Down
3 changes: 1 addition & 2 deletions test-data/unit/check-dataclass-transform.test
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,7 @@ class Bad:
bad1: int = field(alias=some_str()) # E: "alias" argument to dataclass field must be a string literal
bad2: int = field(kw_only=some_bool()) # E: "kw_only" argument must be a boolean literal

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

[typing fixtures/typing-full.pyi]
[builtins fixtures/dataclasses.pyi]
Expand Down

0 comments on commit 17fba49

Please sign in to comment.