diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md index 90c0502f962f0..876e858be7fea 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclass_transform.md @@ -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): ... @@ -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 @@ -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 - -```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 diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index ea809d830a891..14e6e82240c15 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -102,18 +102,34 @@ impl<'db> CodeGeneratorKind<'db> { class: StaticClassLiteral<'db>, specialization: Option>, ) -> Option> { + // 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