[Python-checkins] gh-91860: Add typing.dataclass_transform (PEP 681) (#91861)

JelleZijlstra webhook-mailer at python.org
Mon Apr 25 23:30:55 EDT 2022


https://github.com/python/cpython/commit/5397b5afc1f594dc0ba3b7743351d595e637bf24
commit: 5397b5afc1f594dc0ba3b7743351d595e637bf24
branch: main
author: Jelle Zijlstra <jelle.zijlstra at gmail.com>
committer: JelleZijlstra <jelle.zijlstra at gmail.com>
date: 2022-04-25T20:30:25-07:00
summary:

gh-91860: Add typing.dataclass_transform (PEP 681) (#91861)

Copied from typing-extensions (python/typing#1054, python/typing#1120).

Documentation is intentionally omitted, so we can focus on getting the
runtime part in before the feature freeze.

files:
A Misc/NEWS.d/next/Library/2022-04-23-08-06-36.gh-issue-91860.ityDjK.rst
M Lib/test/test_typing.py
M Lib/typing.py

diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index fce340f27d9ac..a904b7a790c04 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -24,6 +24,7 @@
 from typing import get_origin, get_args
 from typing import is_typeddict
 from typing import reveal_type
+from typing import dataclass_transform
 from typing import no_type_check, no_type_check_decorator
 from typing import Type
 from typing import NamedTuple, NotRequired, Required, TypedDict
@@ -6607,6 +6608,91 @@ def test_reveal_type(self):
         self.assertEqual(stderr.getvalue(), "Runtime type is 'object'\n")
 
 
+class DataclassTransformTests(BaseTestCase):
+    def test_decorator(self):
+        def create_model(*, frozen: bool = False, kw_only: bool = True):
+            return lambda cls: cls
+
+        decorated = dataclass_transform(kw_only_default=True, order_default=False)(create_model)
+
+        class CustomerModel:
+            id: int
+
+        self.assertIs(decorated, create_model)
+        self.assertEqual(
+            decorated.__dataclass_transform__,
+            {
+                "eq_default": True,
+                "order_default": False,
+                "kw_only_default": True,
+                "field_specifiers": (),
+                "kwargs": {},
+            }
+        )
+        self.assertIs(
+            decorated(frozen=True, kw_only=False)(CustomerModel),
+            CustomerModel
+        )
+
+    def test_base_class(self):
+        class ModelBase:
+            def __init_subclass__(cls, *, frozen: bool = False): ...
+
+        Decorated = dataclass_transform(
+            eq_default=True,
+            order_default=True,
+            # Arbitrary unrecognized kwargs are accepted at runtime.
+            make_everything_awesome=True,
+        )(ModelBase)
+
+        class CustomerModel(Decorated, frozen=True):
+            id: int
+
+        self.assertIs(Decorated, ModelBase)
+        self.assertEqual(
+            Decorated.__dataclass_transform__,
+            {
+                "eq_default": True,
+                "order_default": True,
+                "kw_only_default": False,
+                "field_specifiers": (),
+                "kwargs": {"make_everything_awesome": True},
+            }
+        )
+        self.assertIsSubclass(CustomerModel, Decorated)
+
+    def test_metaclass(self):
+        class Field: ...
+
+        class ModelMeta(type):
+            def __new__(
+                cls, name, bases, namespace, *, init: bool = True,
+            ):
+                return super().__new__(cls, name, bases, namespace)
+
+        Decorated = dataclass_transform(
+            order_default=True, field_specifiers=(Field,)
+        )(ModelMeta)
+
+        class ModelBase(metaclass=Decorated): ...
+
+        class CustomerModel(ModelBase, init=False):
+            id: int
+
+        self.assertIs(Decorated, ModelMeta)
+        self.assertEqual(
+            Decorated.__dataclass_transform__,
+            {
+                "eq_default": True,
+                "order_default": True,
+                "kw_only_default": False,
+                "field_specifiers": (Field,),
+                "kwargs": {},
+            }
+        )
+        self.assertIsInstance(CustomerModel, Decorated)
+
+
 class AllTests(BaseTestCase):
     """Tests for __all__."""
 
diff --git a/Lib/typing.py b/Lib/typing.py
index 9d8149c3f5332..f4d4fa4d6713c 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -123,6 +123,7 @@ def _idfunc(_, x):
     'assert_never',
     'cast',
     'clear_overloads',
+    'dataclass_transform',
     'final',
     'get_args',
     'get_origin',
@@ -3271,3 +3272,81 @@ def reveal_type(obj: T, /) -> T:
     """
     print(f"Runtime type is {type(obj).__name__!r}", file=sys.stderr)
     return obj
+
+
+def dataclass_transform(
+    *,
+    eq_default: bool = True,
+    order_default: bool = False,
+    kw_only_default: bool = False,
+    field_specifiers: tuple[type[Any] | Callable[..., Any], ...] = (),
+    **kwargs: Any,
+) -> Callable[[T], T]:
+    """Decorator that marks a function, class, or metaclass as providing
+    dataclass-like behavior.
+
+    Example usage with a decorator function:
+
+        _T = TypeVar("_T")
+
+        @dataclass_transform()
+        def create_model(cls: type[_T]) -> type[_T]:
+            ...
+            return cls
+
+        @create_model
+        class CustomerModel:
+            id: int
+            name: str
+
+    On a base class:
+
+        @dataclass_transform()
+        class ModelBase: ...
+
+        class CustomerModel(ModelBase):
+            id: int
+            name: str
+
+    On a metaclass:
+
+        @dataclass_transform()
+        class ModelMeta(type): ...
+
+        class ModelBase(metaclass=ModelMeta): ...
+
+        class CustomerModel(ModelBase):
+            id: int
+            name: str
+
+    Each of the ``CustomerModel`` classes defined in this example will now
+    behave similarly to a dataclass created with the ``@dataclasses.dataclass``
+    decorator. For example, the type checker will synthesize an ``__init__``
+    method.
+
+    The arguments to this decorator can be used to customize this behavior:
+    - ``eq_default`` indicates whether the ``eq`` parameter is assumed to be
+        True or False if it is omitted by the caller.
+    - ``order_default`` indicates whether the ``order`` parameter is
+        assumed to be True or False if it is omitted by the caller.
+    - ``kw_only_default`` indicates whether the ``kw_only`` parameter is
+        assumed to be True or False if it is omitted by the caller.
+    - ``field_specifiers`` specifies a static list of supported classes
+        or functions that describe fields, similar to ``dataclasses.field()``.
+
+    At runtime, this decorator records its arguments in the
+    ``__dataclass_transform__`` attribute on the decorated object.
+    It has no other runtime effect.
+
+    See PEP 681 for more details.
+    """
+    def decorator(cls_or_fn):
+        cls_or_fn.__dataclass_transform__ = {
+            "eq_default": eq_default,
+            "order_default": order_default,
+            "kw_only_default": kw_only_default,
+            "field_specifiers": field_specifiers,
+            "kwargs": kwargs,
+        }
+        return cls_or_fn
+    return decorator
diff --git a/Misc/NEWS.d/next/Library/2022-04-23-08-06-36.gh-issue-91860.ityDjK.rst b/Misc/NEWS.d/next/Library/2022-04-23-08-06-36.gh-issue-91860.ityDjK.rst
new file mode 100644
index 0000000000000..d5e81c99887ff
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-04-23-08-06-36.gh-issue-91860.ityDjK.rst
@@ -0,0 +1,2 @@
+Add :func:`typing.dataclass_transform`, implementing :pep:`681`. Patch by
+Jelle Zijlstra.



More information about the Python-checkins mailing list