Skip to content

Commit 5397b5a

Browse files
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.
1 parent d174ebe commit 5397b5a

File tree

3 files changed

+167
-0
lines changed

3 files changed

+167
-0
lines changed

Lib/test/test_typing.py

+86
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from typing import get_origin, get_args
2525
from typing import is_typeddict
2626
from typing import reveal_type
27+
from typing import dataclass_transform
2728
from typing import no_type_check, no_type_check_decorator
2829
from typing import Type
2930
from typing import NamedTuple, NotRequired, Required, TypedDict
@@ -6607,6 +6608,91 @@ def test_reveal_type(self):
66076608
self.assertEqual(stderr.getvalue(), "Runtime type is 'object'\n")
66086609

66096610

6611+
class DataclassTransformTests(BaseTestCase):
6612+
def test_decorator(self):
6613+
def create_model(*, frozen: bool = False, kw_only: bool = True):
6614+
return lambda cls: cls
6615+
6616+
decorated = dataclass_transform(kw_only_default=True, order_default=False)(create_model)
6617+
6618+
class CustomerModel:
6619+
id: int
6620+
6621+
self.assertIs(decorated, create_model)
6622+
self.assertEqual(
6623+
decorated.__dataclass_transform__,
6624+
{
6625+
"eq_default": True,
6626+
"order_default": False,
6627+
"kw_only_default": True,
6628+
"field_specifiers": (),
6629+
"kwargs": {},
6630+
}
6631+
)
6632+
self.assertIs(
6633+
decorated(frozen=True, kw_only=False)(CustomerModel),
6634+
CustomerModel
6635+
)
6636+
6637+
def test_base_class(self):
6638+
class ModelBase:
6639+
def __init_subclass__(cls, *, frozen: bool = False): ...
6640+
6641+
Decorated = dataclass_transform(
6642+
eq_default=True,
6643+
order_default=True,
6644+
# Arbitrary unrecognized kwargs are accepted at runtime.
6645+
make_everything_awesome=True,
6646+
)(ModelBase)
6647+
6648+
class CustomerModel(Decorated, frozen=True):
6649+
id: int
6650+
6651+
self.assertIs(Decorated, ModelBase)
6652+
self.assertEqual(
6653+
Decorated.__dataclass_transform__,
6654+
{
6655+
"eq_default": True,
6656+
"order_default": True,
6657+
"kw_only_default": False,
6658+
"field_specifiers": (),
6659+
"kwargs": {"make_everything_awesome": True},
6660+
}
6661+
)
6662+
self.assertIsSubclass(CustomerModel, Decorated)
6663+
6664+
def test_metaclass(self):
6665+
class Field: ...
6666+
6667+
class ModelMeta(type):
6668+
def __new__(
6669+
cls, name, bases, namespace, *, init: bool = True,
6670+
):
6671+
return super().__new__(cls, name, bases, namespace)
6672+
6673+
Decorated = dataclass_transform(
6674+
order_default=True, field_specifiers=(Field,)
6675+
)(ModelMeta)
6676+
6677+
class ModelBase(metaclass=Decorated): ...
6678+
6679+
class CustomerModel(ModelBase, init=False):
6680+
id: int
6681+
6682+
self.assertIs(Decorated, ModelMeta)
6683+
self.assertEqual(
6684+
Decorated.__dataclass_transform__,
6685+
{
6686+
"eq_default": True,
6687+
"order_default": True,
6688+
"kw_only_default": False,
6689+
"field_specifiers": (Field,),
6690+
"kwargs": {},
6691+
}
6692+
)
6693+
self.assertIsInstance(CustomerModel, Decorated)
6694+
6695+
66106696
class AllTests(BaseTestCase):
66116697
"""Tests for __all__."""
66126698

Lib/typing.py

+79
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ def _idfunc(_, x):
123123
'assert_never',
124124
'cast',
125125
'clear_overloads',
126+
'dataclass_transform',
126127
'final',
127128
'get_args',
128129
'get_origin',
@@ -3271,3 +3272,81 @@ def reveal_type(obj: T, /) -> T:
32713272
"""
32723273
print(f"Runtime type is {type(obj).__name__!r}", file=sys.stderr)
32733274
return obj
3275+
3276+
3277+
def dataclass_transform(
3278+
*,
3279+
eq_default: bool = True,
3280+
order_default: bool = False,
3281+
kw_only_default: bool = False,
3282+
field_specifiers: tuple[type[Any] | Callable[..., Any], ...] = (),
3283+
**kwargs: Any,
3284+
) -> Callable[[T], T]:
3285+
"""Decorator that marks a function, class, or metaclass as providing
3286+
dataclass-like behavior.
3287+
3288+
Example usage with a decorator function:
3289+
3290+
_T = TypeVar("_T")
3291+
3292+
@dataclass_transform()
3293+
def create_model(cls: type[_T]) -> type[_T]:
3294+
...
3295+
return cls
3296+
3297+
@create_model
3298+
class CustomerModel:
3299+
id: int
3300+
name: str
3301+
3302+
On a base class:
3303+
3304+
@dataclass_transform()
3305+
class ModelBase: ...
3306+
3307+
class CustomerModel(ModelBase):
3308+
id: int
3309+
name: str
3310+
3311+
On a metaclass:
3312+
3313+
@dataclass_transform()
3314+
class ModelMeta(type): ...
3315+
3316+
class ModelBase(metaclass=ModelMeta): ...
3317+
3318+
class CustomerModel(ModelBase):
3319+
id: int
3320+
name: str
3321+
3322+
Each of the ``CustomerModel`` classes defined in this example will now
3323+
behave similarly to a dataclass created with the ``@dataclasses.dataclass``
3324+
decorator. For example, the type checker will synthesize an ``__init__``
3325+
method.
3326+
3327+
The arguments to this decorator can be used to customize this behavior:
3328+
- ``eq_default`` indicates whether the ``eq`` parameter is assumed to be
3329+
True or False if it is omitted by the caller.
3330+
- ``order_default`` indicates whether the ``order`` parameter is
3331+
assumed to be True or False if it is omitted by the caller.
3332+
- ``kw_only_default`` indicates whether the ``kw_only`` parameter is
3333+
assumed to be True or False if it is omitted by the caller.
3334+
- ``field_specifiers`` specifies a static list of supported classes
3335+
or functions that describe fields, similar to ``dataclasses.field()``.
3336+
3337+
At runtime, this decorator records its arguments in the
3338+
``__dataclass_transform__`` attribute on the decorated object.
3339+
It has no other runtime effect.
3340+
3341+
See PEP 681 for more details.
3342+
"""
3343+
def decorator(cls_or_fn):
3344+
cls_or_fn.__dataclass_transform__ = {
3345+
"eq_default": eq_default,
3346+
"order_default": order_default,
3347+
"kw_only_default": kw_only_default,
3348+
"field_specifiers": field_specifiers,
3349+
"kwargs": kwargs,
3350+
}
3351+
return cls_or_fn
3352+
return decorator
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :func:`typing.dataclass_transform`, implementing :pep:`681`. Patch by
2+
Jelle Zijlstra.

0 commit comments

Comments
 (0)