[ty] Fix generic inference for non-dataclass inheriting from generic dataclass (#21159)

## 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 <noreply@anthropic.com>
Co-authored-by: David Peter <mail@david-peter.de>
This commit is contained in:
Mahmoud Saada 2025-10-31 08:55:17 -04:00 committed by GitHub
parent 3585c96ea5
commit 735ec0c1f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 39 additions and 3 deletions

View file

@ -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 <https://github.com/astral-sh/ty/issues/1427>.
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__`

View file

@ -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<Specialization<'db>>,
inherited_generic_context: Option<GenericContext<'db>>,
name: &str,
) -> Option<Type<'db>> {
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