From 17fba49939d4d8408f77e539290b24dd9b7f07ae Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 23 Feb 2023 22:50:58 +0100 Subject: [PATCH] [1.1 backport] [dataclass_transform] include __dataclass_fields__ in 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 54635dec2379e2ac8b65b6ef07778015c69cfb6a) Co-authored-by: Wesley Collin Wright --- mypy/plugins/dataclasses.py | 19 ++++++++++--------- test-data/unit/check-dataclass-transform.test | 3 +-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/mypy/plugins/dataclasses.py b/mypy/plugins/dataclasses.py index 6b1062d6457f..7694134ac09e 100644 --- a/mypy/plugins/dataclasses.py +++ b/mypy/plugins/dataclasses.py @@ -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] ) diff --git a/test-data/unit/check-dataclass-transform.test b/test-data/unit/check-dataclass-transform.test index 2a7fad1da992..ec87bd4757ed 100644 --- a/test-data/unit/check-dataclass-transform.test +++ b/test-data/unit/check-dataclass-transform.test @@ -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]