Skip to content

Commit 59e5918

Browse files
add dataclass_transform (#1054)
Co-authored-by: Erik De Bonte <[email protected]>
1 parent c811923 commit 59e5918

File tree

4 files changed

+163
-0
lines changed

4 files changed

+163
-0
lines changed

typing_extensions/CHANGELOG

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Release 4.x.x
22

3+
- Runtime support for PEP 681 and `typing_extensions.dataclass_transform`.
34
- `Annotated` can now wrap `ClassVar` and `Final`. Backport from
45
bpo-46491. Patch by Gregory Beauregard (@GBeauregard).
56
- Add missed `Required` and `NotRequired` to `__all__`. Patch by

typing_extensions/README.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ This module currently contains the following:
3737

3838
- Experimental features
3939

40+
- ``@dataclass_transform()`` (see PEP 681)
4041
- ``NotRequired`` (see PEP 655)
4142
- ``Required`` (see PEP 655)
4243
- ``Self`` (see PEP 673)

typing_extensions/src/test_typing_extensions.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from typing_extensions import TypeAlias, ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs, TypeGuard
2323
from typing_extensions import Awaitable, AsyncIterator, AsyncContextManager, Required, NotRequired
2424
from typing_extensions import Protocol, runtime, runtime_checkable, Annotated, overload, final, is_typeddict
25+
from typing_extensions import dataclass_transform
2526
try:
2627
from typing_extensions import get_type_hints
2728
except ImportError:
@@ -2345,6 +2346,83 @@ def cached(self): ...
23452346
self.assertIs(True, Methods.cached.__final__)
23462347

23472348

2349+
class DataclassTransformTests(BaseTestCase):
2350+
def test_decorator(self):
2351+
def create_model(*, frozen: bool = False, kw_only: bool = True):
2352+
return lambda cls: cls
2353+
2354+
decorated = dataclass_transform(kw_only_default=True, order_default=False)(create_model)
2355+
2356+
class CustomerModel:
2357+
id: int
2358+
2359+
self.assertIs(decorated, create_model)
2360+
self.assertEqual(
2361+
decorated.__dataclass_transform__,
2362+
{
2363+
"eq_default": True,
2364+
"order_default": False,
2365+
"kw_only_default": True,
2366+
"field_descriptors": (),
2367+
}
2368+
)
2369+
self.assertIs(
2370+
decorated(frozen=True, kw_only=False)(CustomerModel),
2371+
CustomerModel
2372+
)
2373+
2374+
def test_base_class(self):
2375+
class ModelBase:
2376+
def __init_subclass__(cls, *, frozen: bool = False): ...
2377+
2378+
Decorated = dataclass_transform(eq_default=True, order_default=True)(ModelBase)
2379+
2380+
class CustomerModel(Decorated, frozen=True):
2381+
id: int
2382+
2383+
self.assertIs(Decorated, ModelBase)
2384+
self.assertEqual(
2385+
Decorated.__dataclass_transform__,
2386+
{
2387+
"eq_default": True,
2388+
"order_default": True,
2389+
"kw_only_default": False,
2390+
"field_descriptors": (),
2391+
}
2392+
)
2393+
self.assertIsSubclass(CustomerModel, Decorated)
2394+
2395+
def test_metaclass(self):
2396+
class Field: ...
2397+
2398+
class ModelMeta(type):
2399+
def __new__(
2400+
cls, name, bases, namespace, *, init: bool = True,
2401+
):
2402+
return super().__new__(cls, name, bases, namespace)
2403+
2404+
Decorated = dataclass_transform(
2405+
order_default=True, field_descriptors=(Field,)
2406+
)(ModelMeta)
2407+
2408+
class ModelBase(metaclass=Decorated): ...
2409+
2410+
class CustomerModel(ModelBase, init=False):
2411+
id: int
2412+
2413+
self.assertIs(Decorated, ModelMeta)
2414+
self.assertEqual(
2415+
Decorated.__dataclass_transform__,
2416+
{
2417+
"eq_default": True,
2418+
"order_default": True,
2419+
"kw_only_default": False,
2420+
"field_descriptors": (Field,),
2421+
}
2422+
)
2423+
self.assertIsInstance(CustomerModel, Decorated)
2424+
2425+
23482426
class AllTests(BaseTestCase):
23492427

23502428
def test_typing_extensions_includes_standard(self):

typing_extensions/src/typing_extensions.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def _check_generic(cls, parameters):
7070

7171
# One-off things.
7272
'Annotated',
73+
'dataclass_transform',
7374
'final',
7475
'IntVar',
7576
'is_typeddict',
@@ -2341,3 +2342,85 @@ class Movie(TypedDict):
23412342

23422343
Required = _Required(_root=True)
23432344
NotRequired = _NotRequired(_root=True)
2345+
2346+
if hasattr(typing, 'dataclass_transform'):
2347+
dataclass_transform = typing.dataclass_transform
2348+
else:
2349+
def dataclass_transform(
2350+
*,
2351+
eq_default: bool = True,
2352+
order_default: bool = False,
2353+
kw_only_default: bool = False,
2354+
field_descriptors: typing.Tuple[
2355+
typing.Union[typing.Type[typing.Any], typing.Callable[..., typing.Any]],
2356+
...
2357+
] = (),
2358+
) -> typing.Callable[[T], T]:
2359+
"""Decorator that marks a function, class, or metaclass as providing
2360+
dataclass-like behavior.
2361+
2362+
Example:
2363+
2364+
from typing_extensions import dataclass_transform
2365+
2366+
_T = TypeVar("_T")
2367+
2368+
# Used on a decorator function
2369+
@dataclass_transform()
2370+
def create_model(cls: type[_T]) -> type[_T]:
2371+
...
2372+
return cls
2373+
2374+
@create_model
2375+
class CustomerModel:
2376+
id: int
2377+
name: str
2378+
2379+
# Used on a base class
2380+
@dataclass_transform()
2381+
class ModelBase: ...
2382+
2383+
class CustomerModel(ModelBase):
2384+
id: int
2385+
name: str
2386+
2387+
# Used on a metaclass
2388+
@dataclass_transform()
2389+
class ModelMeta(type): ...
2390+
2391+
class ModelBase(metaclass=ModelMeta): ...
2392+
2393+
class CustomerModel(ModelBase):
2394+
id: int
2395+
name: str
2396+
2397+
Each of the ``CustomerModel`` classes defined in this example will now
2398+
behave similarly to a dataclass created with the ``@dataclasses.dataclass``
2399+
decorator. For example, the type checker will synthesize an ``__init__``
2400+
method.
2401+
2402+
The arguments to this decorator can be used to customize this behavior:
2403+
- ``eq_default`` indicates whether the ``eq`` parameter is assumed to be
2404+
True or False if it is omitted by the caller.
2405+
- ``order_default`` indicates whether the ``order`` parameter is
2406+
assumed to be True or False if it is omitted by the caller.
2407+
- ``kw_only_default`` indicates whether the ``kw_only`` parameter is
2408+
assumed to be True or False if it is omitted by the caller.
2409+
- ``field_descriptors`` specifies a static list of supported classes
2410+
or functions, that describe fields, similar to ``dataclasses.field()``.
2411+
2412+
At runtime, this decorator records its arguments in the
2413+
``__dataclass_transform__`` attribute on the decorated object.
2414+
2415+
See PEP 681 for details.
2416+
2417+
"""
2418+
def decorator(cls_or_fn):
2419+
cls_or_fn.__dataclass_transform__ = {
2420+
"eq_default": eq_default,
2421+
"order_default": order_default,
2422+
"kw_only_default": kw_only_default,
2423+
"field_descriptors": field_descriptors,
2424+
}
2425+
return cls_or_fn
2426+
return decorator

0 commit comments

Comments
 (0)