Skip to content

Commit 735ec0c

Browse files
saadaclaudesharkdp
authored
[ty] Fix generic inference for non-dataclass inheriting from generic dataclass (#21159)
## Summary Fixes astral-sh/ty#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 <noreply@anthropic.com> Co-authored-by: David Peter <mail@david-peter.de>
1 parent 3585c96 commit 735ec0c

File tree

2 files changed

+39
-3
lines changed

2 files changed

+39
-3
lines changed

crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,40 @@ class WrappedIntAndExtraData[T](Wrap[int]):
838838
reveal_type(WrappedIntAndExtraData[bytes].__init__)
839839
```
840840

841+
### Non-dataclass inheriting from generic dataclass
842+
843+
This is a regression test for <https://github.com/astral-sh/ty/issues/1427>.
844+
845+
When a non-dataclass inherits from a generic dataclass, the generic type parameters should still be
846+
properly inferred when calling the inherited `__init__` method.
847+
848+
```py
849+
from dataclasses import dataclass
850+
851+
@dataclass
852+
class ParentDataclass[T]:
853+
value: T
854+
855+
# Non-dataclass inheriting from generic dataclass
856+
class ChildOfParentDataclass[T](ParentDataclass[T]): ...
857+
858+
def uses_dataclass[T](x: T) -> ChildOfParentDataclass[T]:
859+
return ChildOfParentDataclass(x)
860+
861+
# TODO: ParentDataclass.__init__ should show generic types, not Unknown
862+
# revealed: (self: ParentDataclass[Unknown], value: Unknown) -> None
863+
reveal_type(ParentDataclass.__init__)
864+
865+
# revealed: (self: ParentDataclass[T@ChildOfParentDataclass], value: T@ChildOfParentDataclass) -> None
866+
reveal_type(ChildOfParentDataclass.__init__)
867+
868+
result_int = uses_dataclass(42)
869+
reveal_type(result_int) # revealed: ChildOfParentDataclass[Literal[42]]
870+
871+
result_str = uses_dataclass("hello")
872+
reveal_type(result_str) # revealed: ChildOfParentDataclass[Literal["hello"]]
873+
```
874+
841875
## Descriptor-typed fields
842876

843877
### Same type in `__get__` and `__set__`

crates/ty_python_semantic/src/types/class.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2176,7 +2176,8 @@ impl<'db> ClassLiteral<'db> {
21762176
});
21772177

21782178
if member.is_undefined() {
2179-
if let Some(synthesized_member) = self.own_synthesized_member(db, specialization, name)
2179+
if let Some(synthesized_member) =
2180+
self.own_synthesized_member(db, specialization, inherited_generic_context, name)
21802181
{
21812182
return Member::definitely_declared(synthesized_member);
21822183
}
@@ -2192,6 +2193,7 @@ impl<'db> ClassLiteral<'db> {
21922193
self,
21932194
db: &'db dyn Db,
21942195
specialization: Option<Specialization<'db>>,
2196+
inherited_generic_context: Option<GenericContext<'db>>,
21952197
name: &str,
21962198
) -> Option<Type<'db>> {
21972199
let dataclass_params = self.dataclass_params(db);
@@ -2320,7 +2322,7 @@ impl<'db> ClassLiteral<'db> {
23202322

23212323
let signature = match name {
23222324
"__new__" | "__init__" => Signature::new_generic(
2323-
self.inherited_generic_context(db),
2325+
inherited_generic_context.or_else(|| self.inherited_generic_context(db)),
23242326
Parameters::new(parameters),
23252327
return_ty,
23262328
),
@@ -2702,7 +2704,7 @@ impl<'db> ClassLiteral<'db> {
27022704
name: &str,
27032705
policy: MemberLookupPolicy,
27042706
) -> PlaceAndQualifiers<'db> {
2705-
if let Some(member) = self.own_synthesized_member(db, specialization, name) {
2707+
if let Some(member) = self.own_synthesized_member(db, specialization, None, name) {
27062708
Place::bound(member).into()
27072709
} else {
27082710
KnownClass::TypedDictFallback

0 commit comments

Comments
 (0)