summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJelle Zijlstra <jelle.zijlstra@gmail.com>2022-04-26 03:30:25 (GMT)
committerGitHub <noreply@github.com>2022-04-26 03:30:25 (GMT)
commit5397b5afc1f594dc0ba3b7743351d595e637bf24 (patch)
tree4faba19b9efee47101ecf76bdd62fd7e0555db55
parentd174ebe91ebc9f7388a22cc81cdc5f7be8bb8c9b (diff)
downloadcpython-5397b5afc1f594dc0ba3b7743351d595e637bf24.zip
cpython-5397b5afc1f594dc0ba3b7743351d595e637bf24.tar.gz
cpython-5397b5afc1f594dc0ba3b7743351d595e637bf24.tar.bz2
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.
-rw-r--r--Lib/test/test_typing.py86
-rw-r--r--Lib/typing.py79
-rw-r--r--Misc/NEWS.d/next/Library/2022-04-23-08-06-36.gh-issue-91860.ityDjK.rst2
3 files changed, 167 insertions, 0 deletions
diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py
index fce340f..a904b7a 100644
--- a/Lib/test/test_typing.py
+++ b/Lib/test/test_typing.py
@@ -24,6 +24,7 @@ from typing import get_type_hints
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 @@ class RevealTypeTests(BaseTestCase):
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 9d8149c..f4d4fa4 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -123,6 +123,7 @@ __all__ = [
'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 0000000..d5e81c9
--- /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.