ruff/crates/ty_python_semantic/resources/mdtest/dataclass_transform.md
Abhijeet Prasad Bodas 3dedd70a92
[ty] Detect overloads decorated with @dataclass_transform (#17835)
## Summary

Fixes #17541

Before this change, in the case of overloaded functions,
`@dataclass_transform` was detected only when applied to the
implementation, not the overloads.
However, the spec also allows this decorator to be applied to any of the
overloads as well.
With this PR, we start handling `@dataclass_transform`s applied to
overloads.

## Test Plan

Fixed existing TODOs in the test suite.
2025-05-07 15:51:13 +02:00

5.5 KiB

typing.dataclass_transform

[environment]
python-version = "3.12"

dataclass_transform is a decorator that can be used to let type checkers know that a function, class, or metaclass is a dataclass-like construct.

Basic example

from typing_extensions import dataclass_transform

@dataclass_transform()
def my_dataclass[T](cls: type[T]) -> type[T]:
    # modify cls
    return cls

@my_dataclass
class Person:
    name: str
    age: int | None = None

Person("Alice", 20)
Person("Bob", None)
Person("Bob")

# error: [missing-argument]
Person()

Decorating decorators that take parameters themselves

If we want our dataclass-like decorator to also take parameters, that is also possible:

from typing_extensions import dataclass_transform, Callable

@dataclass_transform()
def versioned_class[T](*, version: int = 1):
    def decorator(cls):
        # modify cls
        return cls
    return decorator

@versioned_class(version=2)
class Person:
    name: str
    age: int | None = None

Person("Alice", 20)

# error: [missing-argument]
Person()

We properly type-check the arguments to the decorator:

from typing_extensions import dataclass_transform, Callable

# error: [invalid-argument-type]
@versioned_class(version="a string")
class C:
    name: str

Types of decorators

The examples from this section are straight from the Python documentation on typing.dataclass_transform.

Decorating a decorator function

from typing_extensions import dataclass_transform

@dataclass_transform()
def create_model[T](cls: type[T]) -> type[T]:
    ...
    return cls

@create_model
class CustomerModel:
    id: int
    name: str

CustomerModel(id=1, name="Test")

Decorating a metaclass

from typing_extensions import dataclass_transform

@dataclass_transform()
class ModelMeta(type): ...

class ModelBase(metaclass=ModelMeta): ...

class CustomerModel(ModelBase):
    id: int
    name: str

CustomerModel(id=1, name="Test")

# error: [missing-argument]
CustomerModel()

Decorating a base class

from typing_extensions import dataclass_transform

@dataclass_transform()
class ModelBase: ...

class CustomerModel(ModelBase):
    id: int
    name: str

# TODO: this is not supported yet
# error: [unknown-argument]
# error: [unknown-argument]
CustomerModel(id=1, name="Test")

Arguments to dataclass_transform

eq_default

eq=True/False does not have a observable effect (apart from a minor change regarding whether other is positional-only or not, which is not modelled at the moment).

order_default

The order_default argument controls whether methods such as __lt__ are generated by default. This can be overwritten using the order argument to the custom decorator:

from typing_extensions import dataclass_transform

@dataclass_transform()
def normal(*, order: bool = False):
    raise NotImplementedError

@dataclass_transform(order_default=False)
def order_default_false(*, order: bool = False):
    raise NotImplementedError

@dataclass_transform(order_default=True)
def order_default_true(*, order: bool = True):
    raise NotImplementedError

@normal
class Normal:
    inner: int

Normal(1) < Normal(2)  # error: [unsupported-operator]

@normal(order=True)
class NormalOverwritten:
    inner: int

NormalOverwritten(1) < NormalOverwritten(2)

@order_default_false
class OrderFalse:
    inner: int

OrderFalse(1) < OrderFalse(2)  # error: [unsupported-operator]

@order_default_false(order=True)
class OrderFalseOverwritten:
    inner: int

OrderFalseOverwritten(1) < OrderFalseOverwritten(2)

@order_default_true
class OrderTrue:
    inner: int

OrderTrue(1) < OrderTrue(2)

@order_default_true(order=False)
class OrderTrueOverwritten:
    inner: int

# error: [unsupported-operator]
OrderTrueOverwritten(1) < OrderTrueOverwritten(2)

kw_only_default

To do

field_specifiers

To do

Overloaded dataclass-like decorators

In the case of an overloaded decorator, the dataclass_transform decorator can be applied to the implementation, or to one of the overloads.

Applying dataclass_transform to the implementation

from typing_extensions import dataclass_transform, TypeVar, Callable, overload

T = TypeVar("T", bound=type)

@overload
def versioned_class(
    cls: T,
    *,
    version: int = 1,
) -> T: ...
@overload
def versioned_class(
    *,
    version: int = 1,
) -> Callable[[T], T]: ...
@dataclass_transform()
def versioned_class(
    cls: T | None = None,
    *,
    version: int = 1,
) -> T | Callable[[T], T]:
    raise NotImplementedError

@versioned_class
class D1:
    x: str

@versioned_class(version=2)
class D2:
    x: str

D1("a")
D2("a")

D1(1.2)  # error: [invalid-argument-type]
D2(1.2)  # error: [invalid-argument-type]

Applying dataclass_transform to an overload

from typing_extensions import dataclass_transform, TypeVar, Callable, overload

T = TypeVar("T", bound=type)

@overload
@dataclass_transform()
def versioned_class(
    cls: T,
    *,
    version: int = 1,
) -> T: ...
@overload
def versioned_class(
    *,
    version: int = 1,
) -> Callable[[T], T]: ...
def versioned_class(
    cls: T | None = None,
    *,
    version: int = 1,
) -> T | Callable[[T], T]:
    raise NotImplementedError

@versioned_class
class D1:
    x: str

@versioned_class(version=2)
class D2:
    x: str

D1("a")
D2("a")

D1(1.2)  # error: [invalid-argument-type]
D2(1.2)  # error: [invalid-argument-type]