-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[ty] Correctly instantiate generic class that inherits __init__ from generic base class
#19693
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Diagnostic diff on typing conformance testsNo changes detected when running ty on typing conformance tests ✅ |
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! I think you also need to pass the generic context into the __new__ method we synthesize for tuple types here:
ruff/crates/ty_python_semantic/src/types/class.rs
Lines 783 to 827 in 06cd249
| // ```py | |
| // class tuple: | |
| // @overload | |
| // def __new__(cls: type[tuple[()]], iterable: tuple[()] = ()) -> tuple[()]: ... | |
| // @overload | |
| // def __new__[T](cls: type[tuple[T, ...]], iterable: tuple[T, ...]) -> tuple[T, ...]: ... | |
| // ``` | |
| "__new__" if class_literal.is_tuple(db) => { | |
| let mut iterable_parameter = | |
| Parameter::positional_only(Some(Name::new_static("iterable"))); | |
| match specialization { | |
| Some(spec) => { | |
| // TODO: Once we support PEP 646 annotations for `*args` parameters, we can | |
| // use the tuple itself as the argument type. | |
| let tuple = spec.tuple(db); | |
| let tuple_len = tuple.len(); | |
| if tuple_len.minimum() == 0 && tuple_len.maximum().is_none() { | |
| // If the tuple has no length restrictions, | |
| // any iterable is allowed as long as the iterable has the correct element type. | |
| let mut tuple_elements = tuple.all_elements(); | |
| iterable_parameter = iterable_parameter.with_annotated_type( | |
| KnownClass::Iterable | |
| .to_specialized_instance(db, [*tuple_elements.next().unwrap()]), | |
| ); | |
| assert_eq!( | |
| tuple_elements.next(), | |
| None, | |
| "Tuple specialization should not have more than one element when it has no length restriction" | |
| ); | |
| } else { | |
| // But if the tuple is of a fixed length, or has a minimum length, we require a tuple rather | |
| // than an iterable, as a tuple is the only kind of iterable for which we can | |
| // specify a fixed length, or that the iterable must be at least a certain length. | |
| iterable_parameter = | |
| iterable_parameter.with_annotated_type(Type::instance(db, self)); | |
| } | |
| } | |
| None => { | |
| // If the tuple isn't specialized at all, we allow any argument as long as it is iterable. | |
| iterable_parameter = iterable_parameter | |
| .with_annotated_type(KnownClass::Iterable.to_instance(db)); | |
| } | |
| } |
Example failing test case on your branch:
class Foo[T, U](tuple[T, U]): ... # false positive error: Argument is incorrect: Expected `tuple[T@Foo, U@Foo]`, found `tuple[Literal[1], Literal[2]]` (invalid-argument-type)
reveal_type(Foo((1, 2))) # revealed: Foo[Unknown, Unknown]|
Love that primer report!! |
Thank you! Done |
|
|
||
| class C(tuple[T, U]): ... | ||
|
|
||
| reveal_type(C((1, 2))) # revealed: C[int, int] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I sort of wish that this was C[Literal[1], Literal[2]] -- I don't think there's a strong reason to upcast the Literal types to int types here, since tuple is covariant. We'd obviously infer tuple[Literal[1], Literal[2]] rather than tuple[int, int] for (1, 2), for similar reasons: the covariance of tuple means that tuple[Literal[1], Literal[2]] is assignable to tuple[int, int], so there's no way that inferring the more precise type can produce false positives.
But that's not for this PR! It's obviously a pre-existing issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep totally!
This is subtle, and the root cause became more apparent with #19604, since we now have many more cases of superclasses and subclasses using different typevars. The issue is easiest to see in the following:
When instantiating a generic class, the
__init__method inherits the generic context of that class. This lets our call binding machinery infer a specialization for that context.Prior to this PR, the instantiation of
Cworked just fine. Its__init__method would inherit the[T]generic context, and we would infer{T = int}as the specialization based on the argument parameters.It didn't work for
D. The issue is that the__init__method was inheriting the generic context of the class where__init__was defined (here,Cand[T]). At the call site, we would then infer{T = int}as the specialization — but that wouldn't help us specializeD[U], sinceDdoes not haveTin its generic context!Instead, the
__init__method should inherit the generic context of the class that we are performing the lookup on (here,Dand[U]). That lets us correctly infer{U = int}as the specialization, which we can successfully apply toD[U].(Note that
__init__refers toC's typevars in its signature, but that's okay; our member lookup logic already applies theT = Uspecialization when returning a member ofCwhile performing a lookup onD, transforming its signature from(Self, T) -> Noneto(Self, U) -> None.)Closes astral-sh/ty#588