From 735ec0c1f97d5b80f1161835ffb3a784d3eebcac Mon Sep 17 00:00:00 2001 From: Mahmoud Saada Date: Fri, 31 Oct 2025 08:55:17 -0400 Subject: [PATCH] [ty] Fix generic inference for non-dataclass inheriting from generic dataclass (#21159) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes https://github.com/astral-sh/ty/issues/1427 This PR fixes a regression introduced in alpha.24 where non-dataclass children of generic dataclasses lost generic type parameter information during `__init__` synthesis. The issue occurred because when looking up inherited members in the MRO, the child class's `inherited_generic_context` was correctly passed down, but `own_synthesized_member()` (which synthesizes dataclass `__init__` methods) didn't accept this parameter. It only used `self.inherited_generic_context(db)`, which returned the parent's context instead of the child's. The fix threads the child's generic context through to the synthesis logic, allowing proper generic type inference for inherited dataclass constructors. ## Test Plan - Added regression test for non-dataclass inheriting from generic dataclass - Verified the exact repro case from the issue now works - All 277 mdtest tests passing - Clippy clean - Manually verified with Python runtime, mypy, and pyright - all accept this code pattern ## Verification Tested against multiple type checkers: - ✅ Python runtime: Code works correctly - ✅ mypy: No issues found - ✅ pyright: 0 errors, 0 warnings - ✅ ty alpha.23: Worked (before regression) - ❌ ty alpha.24: Regression - ✅ ty with this fix: Works correctly --------- Co-authored-by: Claude Co-authored-by: David Peter --- .../mdtest/dataclasses/dataclasses.md | 34 +++++++++++++++++++ crates/ty_python_semantic/src/types/class.rs | 8 +++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md index e7171b6dd4..d8619851a2 100644 --- a/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md +++ b/crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md @@ -838,6 +838,40 @@ class WrappedIntAndExtraData[T](Wrap[int]): reveal_type(WrappedIntAndExtraData[bytes].__init__) ``` +### Non-dataclass inheriting from generic dataclass + +This is a regression test for . + +When a non-dataclass inherits from a generic dataclass, the generic type parameters should still be +properly inferred when calling the inherited `__init__` method. + +```py +from dataclasses import dataclass + +@dataclass +class ParentDataclass[T]: + value: T + +# Non-dataclass inheriting from generic dataclass +class ChildOfParentDataclass[T](ParentDataclass[T]): ... + +def uses_dataclass[T](x: T) -> ChildOfParentDataclass[T]: + return ChildOfParentDataclass(x) + +# TODO: ParentDataclass.__init__ should show generic types, not Unknown +# revealed: (self: ParentDataclass[Unknown], value: Unknown) -> None +reveal_type(ParentDataclass.__init__) + +# revealed: (self: ParentDataclass[T@ChildOfParentDataclass], value: T@ChildOfParentDataclass) -> None +reveal_type(ChildOfParentDataclass.__init__) + +result_int = uses_dataclass(42) +reveal_type(result_int) # revealed: ChildOfParentDataclass[Literal[42]] + +result_str = uses_dataclass("hello") +reveal_type(result_str) # revealed: ChildOfParentDataclass[Literal["hello"]] +``` + ## Descriptor-typed fields ### Same type in `__get__` and `__set__` diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index 75190a3c3a..4f8ee4c1fc 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -2176,7 +2176,8 @@ impl<'db> ClassLiteral<'db> { }); if member.is_undefined() { - if let Some(synthesized_member) = self.own_synthesized_member(db, specialization, name) + if let Some(synthesized_member) = + self.own_synthesized_member(db, specialization, inherited_generic_context, name) { return Member::definitely_declared(synthesized_member); } @@ -2192,6 +2193,7 @@ impl<'db> ClassLiteral<'db> { self, db: &'db dyn Db, specialization: Option>, + inherited_generic_context: Option>, name: &str, ) -> Option> { let dataclass_params = self.dataclass_params(db); @@ -2320,7 +2322,7 @@ impl<'db> ClassLiteral<'db> { let signature = match name { "__new__" | "__init__" => Signature::new_generic( - self.inherited_generic_context(db), + inherited_generic_context.or_else(|| self.inherited_generic_context(db)), Parameters::new(parameters), return_ty, ), @@ -2702,7 +2704,7 @@ impl<'db> ClassLiteral<'db> { name: &str, policy: MemberLookupPolicy, ) -> PlaceAndQualifiers<'db> { - if let Some(member) = self.own_synthesized_member(db, specialization, name) { + if let Some(member) = self.own_synthesized_member(db, specialization, None, name) { Place::bound(member).into() } else { KnownClass::TypedDictFallback