Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -6594,6 +6595,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__."""

Expand Down
79 changes: 79 additions & 0 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ def _idfunc(_, x):
'assert_never',
'cast',
'clear_overloads',
'dataclass_transform',
'final',
'get_args',
'get_origin',
Expand Down Expand Up @@ -3265,3 +3266,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :func:`typing.dataclass_transform`, implementing :pep:`681`. Patch by
Jelle Zijlstra.