mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-19 20:24:27 +00:00
[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:
parent
3585c96ea5
commit
735ec0c1f9
2 changed files with 39 additions and 3 deletions
|
|
@ -838,6 +838,40 @@ class WrappedIntAndExtraData[T](Wrap[int]):
|
||||||
reveal_type(WrappedIntAndExtraData[bytes].__init__)
|
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
|
## Descriptor-typed fields
|
||||||
|
|
||||||
### Same type in `__get__` and `__set__`
|
### Same type in `__get__` and `__set__`
|
||||||
|
|
|
||||||
|
|
@ -2176,7 +2176,8 @@ impl<'db> ClassLiteral<'db> {
|
||||||
});
|
});
|
||||||
|
|
||||||
if member.is_undefined() {
|
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);
|
return Member::definitely_declared(synthesized_member);
|
||||||
}
|
}
|
||||||
|
|
@ -2192,6 +2193,7 @@ impl<'db> ClassLiteral<'db> {
|
||||||
self,
|
self,
|
||||||
db: &'db dyn Db,
|
db: &'db dyn Db,
|
||||||
specialization: Option<Specialization<'db>>,
|
specialization: Option<Specialization<'db>>,
|
||||||
|
inherited_generic_context: Option<GenericContext<'db>>,
|
||||||
name: &str,
|
name: &str,
|
||||||
) -> Option<Type<'db>> {
|
) -> Option<Type<'db>> {
|
||||||
let dataclass_params = self.dataclass_params(db);
|
let dataclass_params = self.dataclass_params(db);
|
||||||
|
|
@ -2320,7 +2322,7 @@ impl<'db> ClassLiteral<'db> {
|
||||||
|
|
||||||
let signature = match name {
|
let signature = match name {
|
||||||
"__new__" | "__init__" => Signature::new_generic(
|
"__new__" | "__init__" => Signature::new_generic(
|
||||||
self.inherited_generic_context(db),
|
inherited_generic_context.or_else(|| self.inherited_generic_context(db)),
|
||||||
Parameters::new(parameters),
|
Parameters::new(parameters),
|
||||||
return_ty,
|
return_ty,
|
||||||
),
|
),
|
||||||
|
|
@ -2702,7 +2704,7 @@ impl<'db> ClassLiteral<'db> {
|
||||||
name: &str,
|
name: &str,
|
||||||
policy: MemberLookupPolicy,
|
policy: MemberLookupPolicy,
|
||||||
) -> PlaceAndQualifiers<'db> {
|
) -> 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()
|
Place::bound(member).into()
|
||||||
} else {
|
} else {
|
||||||
KnownClass::TypedDictFallback
|
KnownClass::TypedDictFallback
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue