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.
This commit is contained in:
Jelle Zijlstra 2022-04-25 20:30:25 -07:00 committed by GitHub
parent d174ebe91e
commit 5397b5afc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 167 additions and 0 deletions

View File

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

View File

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

View File

@ -0,0 +1,2 @@
Add :func:`typing.dataclass_transform`, implementing :pep:`681`. Patch by
Jelle Zijlstra.