Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,11 @@ CustomerModel(id=1, name="Test")

### Decorating a metaclass

If the metaclass of a class `A` is decorated with `@dataclass_transform`, `A` will have
dataclass-like semantics.

```py
from typing_extensions import dataclass_transform
from typing_extensions import Any, dataclass_transform

@dataclass_transform()
class ModelMeta(type): ...
Expand All @@ -110,6 +113,45 @@ CustomerModel(id=1, name="Test")
CustomerModel()
```

This is also true if the metaclass is a subclass of a class decorated with `@dataclass_transform`:

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

class RegistryMeta(ModelMeta): ...
class ModelBase(metaclass=RegistryMeta): ...

class Person(ModelBase):
name: str

reveal_type(Person.__init__) # revealed: (self: Person, name: str) -> None

Person("Alice")
Person(name="Alice")

# error: [missing-argument]
Person()

# error: [unknown-argument]
Person(name="Alice", extra=1)
```

But when a subclass of `type` is decorated with `@dataclass_transform`, we do not consider its
subclasses to themselves be dataclasses; this would break the above case. If `RegistryMeta` were
given dataclass semantics itself, it would no longer be usable as a metaclass, since its `__init__`
would be overridden with a dataclass-style `__init__` method, instead of the `type.__init__`
signature.

This is an unclear area in the typing spec, which should be clarified. Pyright does the opposite
(treats `RegistryMeta` as a dataclass, but does not treat `Person` as a dataclass), but that seems
less useful in practice, and Pydantic relies on the behavior we implement.

```py
# revealed: Overload[(self, o: object, /) -> None, (self, name: str, bases: tuple[type, ...], dict: dict[str, Any], /, **kwds: Any) -> None]
reveal_type(RegistryMeta.__init__)
```

### Decorating a base class

```py
Expand Down Expand Up @@ -1033,36 +1075,6 @@ Person("Alice", 30, [], "some notes", email="alice@example.com")
Person("Bob", email="bob@example.com", notes="other notes")
```

#### Inherited metaclass-based transformer
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we moved this test up, because it seemed to incorrectly be in the "Field specifiers using **kwargs" section, despite not having much to do with field specifiers that use **kwargs


```py
from typing import Any, dataclass_transform

def field(*, default: Any = ...) -> Any: ...

@dataclass_transform(field_specifiers=(field,))
class ModelMeta(type): ...

class RegistryMeta(ModelMeta): ...
class ModelBase(metaclass=RegistryMeta): ...

class Person(ModelBase):
name: str
age: int = field(default=0)

reveal_type(Person.__init__) # revealed: (self: Person, name: str, age: int = ...) -> None

Person("Alice")
Person("Alice", 30)
Person(name="Alice", age=30)

# error: [missing-argument]
Person(age=30)

# error: [unknown-argument]
Person(name="Alice", extra=1)
```

#### Base-class-based transformer

```py
Expand Down
30 changes: 23 additions & 7 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,18 +102,34 @@ impl<'db> CodeGeneratorKind<'db> {
class: StaticClassLiteral<'db>,
specialization: Option<Specialization<'db>>,
) -> Option<CodeGeneratorKind<'db>> {
// If a class is directly decorated as a dataclass, it's a dataclass.
// If a class' metaclass is a dataclass transformer, it's a dataclass.
// If a class inherits from a base class that is a dataclass
// transformer, it's a dataclass (unless it is a subclass of `type`,
// in which case we assume the subclass is itself also meant for use
// as a metaclass dataclass transformer, not itself supposed to be a
// dataclass.)
if class.dataclass_params(db).is_some() {
Some(CodeGeneratorKind::DataclassLike(None))
} else if let Ok((_, Some(info))) = class.try_metaclass(db) {
Some(CodeGeneratorKind::DataclassLike(Some(info.params)))
} else if let Some(transformer_params) =
class.iter_mro(db, specialization).skip(1).find_map(|base| {
base.into_class().and_then(|class| {
class
.static_class_literal(db)
.and_then(|(lit, _)| lit.dataclass_transformer_params(db))
})
} else if KnownClass::Type
.try_to_class_literal(db)
.is_none_or(|type_class| {
!class.is_subclass_of(
db,
None,
ClassType::NonGeneric(ClassLiteral::Static(type_class)),
)
})
&& let Some(transformer_params) =
class.iter_mro(db, specialization).skip(1).find_map(|base| {
base.into_class().and_then(|class| {
class
.static_class_literal(db)
.and_then(|(lit, _)| lit.dataclass_transformer_params(db))
})
})
{
Some(CodeGeneratorKind::DataclassLike(Some(transformer_params)))
} else if class
Expand Down
Loading