Skip to content

Commit

Permalink
gh-91860: Add typing.dataclass_transform (PEP 681) (#91861)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
JelleZijlstra committed Apr 26, 2022
1 parent d174ebe commit 5397b5a
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 0 deletions.
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 @@ -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__."""

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 @@ -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
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.

0 comments on commit 5397b5a

Please sign in to comment.