-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[ty] Simple subtyping support for bidirectional inference #21747
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
base: main
Are you sure you want to change the base?
Conversation
51d8ef8 to
0e577a3
Compare
Diagnostic diff on typing conformance testsChanges were detected when running ty on typing conformance tests--- old-output.txt 2025-12-02 22:03:19.784386714 +0000
+++ new-output.txt 2025-12-02 22:03:23.396430298 +0000
@@ -815,7 +815,7 @@
overloads_evaluation.py:291:33: error[invalid-return-type] Function always implicitly returns `None`, which is not assignable to return type `T@example6`
protocols_class_objects.py:58:16: error[invalid-assignment] Object of type `<class 'ConcreteA'>` is not assignable to `ProtoA1`
protocols_class_objects.py:59:16: error[invalid-assignment] Object of type `<class 'ConcreteA'>` is not assignable to `ProtoA2`
-protocols_definition.py:30:11: error[invalid-argument-type] Argument to function `close_all` is incorrect: Expected `Iterable[SupportsClose]`, found `list[Unknown | int]`
+protocols_definition.py:30:11: error[invalid-argument-type] Argument to function `close_all` is incorrect: Expected `Iterable[SupportsClose]`, found `list[SupportsClose | int]`
protocols_definition.py:114:22: error[invalid-assignment] Object of type `Concrete2_Bad1` is not assignable to `Template2`
protocols_definition.py:115:22: error[invalid-assignment] Object of type `Concrete2_Bad2` is not assignable to `Template2`
protocols_definition.py:116:22: error[invalid-assignment] Object of type `Concrete2_Bad3` is not assignable to `Template2`
|
|
|
| Lint rule | Added | Removed | Changed |
|---|---|---|---|
invalid-argument-type |
26 | 80 | 168 |
invalid-assignment |
5 | 25 | 66 |
unsupported-operator |
0 | 1 | 39 |
type-assertion-failure |
0 | 13 | 21 |
invalid-return-type |
1 | 4 | 14 |
possibly-missing-attribute |
2 | 11 | 2 |
unresolved-attribute |
14 | 0 | 0 |
no-matching-overload |
7 | 5 | 0 |
unused-ignore-comment |
5 | 6 | 0 |
non-subscriptable |
1 | 4 | 0 |
not-iterable |
0 | 2 | 0 |
unsupported-base |
0 | 1 | 0 |
| Total | 61 | 152 | 310 |
crates/ty_python_semantic/resources/mdtest/type_compendium/tuple.md
Outdated
Show resolved
Hide resolved
CodSpeed Performance ReportMerging #21747 will degrade performances by 7.79%Comparing Summary
Benchmarks breakdown
Footnotes
|
ceff10e to
83ba34d
Compare
83ba34d to
19c5cd8
Compare
AlexWaygood
left a comment
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, this looks great
| // TODO: We should use the constraint solver here to determine the type mappings for more | ||
| // complex subtyping relationships, e.g., `type[C[T]]` to `Callable[..., T]`, or unions | ||
| // containing multiple generic elements. |
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.
you could possibly also mention protocols here, since although this PR tackles simple cases where a nominal type actually subclasses a protocol, there are also lots of cases where a nominal type is a subtype of a protocol without actually subclassing that protocol
| timestamp: datetime = field(default_factory=datetime.now, init=False) | ||
|
|
||
| # revealed: (self: Data, content: list[int] = list[int]) -> None | ||
| # revealed: (self: Data, content: list[int] = Unknown) -> None |
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.
any idea what's going on here?
| for (base_typevar, base_ty) in specialization | ||
| .generic_context(db) | ||
| .variables(db) | ||
| .zip(specialization.types(db)) | ||
| { | ||
| if *base_ty == Type::TypeVar(bound_typevar) { | ||
| if !visited.insert((origin, base_typevar.identity(db))) { | ||
| return None; | ||
| } | ||
|
|
||
| if let Some(ty) = | ||
| self.find_type_var_from_impl(db, base_typevar, origin, visited) | ||
| { | ||
| return Some(ty); | ||
| } | ||
| } | ||
| } |
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.
we do a similar loop through the MRO in the (old) constraint solver which doesn't seem to need to do the FxHashSet memoization thing -- could you also take a similar approach here? Might that be more performant?
ruff/crates/ty_python_semantic/src/types/generics.rs
Lines 1598 to 1641 in 2250fa6
| // Extract formal_alias if this is a generic class | |
| let formal_alias = match formal { | |
| Type::NominalInstance(formal_nominal) => { | |
| formal_nominal.class(self.db).into_generic_alias() | |
| } | |
| // TODO: This will only handle classes that explicit implement a generic protocol | |
| // by listing it as a base class. To handle classes that implicitly implement a | |
| // generic protocol, we will need to check the types of the protocol members to be | |
| // able to infer the specialization of the protocol that the class implements. | |
| Type::ProtocolInstance(ProtocolInstanceType { | |
| inner: Protocol::FromClass(class), | |
| .. | |
| }) => class.into_generic_alias(), | |
| _ => None, | |
| }; | |
| if let Some(formal_alias) = formal_alias { | |
| let formal_origin = formal_alias.origin(self.db); | |
| for base in actual_nominal.class(self.db).iter_mro(self.db) { | |
| let ClassBase::Class(ClassType::Generic(base_alias)) = base else { | |
| continue; | |
| }; | |
| if formal_origin != base_alias.origin(self.db) { | |
| continue; | |
| } | |
| let generic_context = formal_alias | |
| .specialization(self.db) | |
| .generic_context(self.db) | |
| .variables(self.db); | |
| let formal_specialization = | |
| formal_alias.specialization(self.db).types(self.db); | |
| let base_specialization = base_alias.specialization(self.db).types(self.db); | |
| for (typevar, formal_ty, base_ty) in itertools::izip!( | |
| generic_context, | |
| formal_specialization, | |
| base_specialization | |
| ) { | |
| let variance = typevar.variance_with_polarity(self.db, polarity); | |
| self.infer_map_impl(*formal_ty, *base_ty, variance, &mut f)?; | |
| } | |
| return Ok(()); | |
| } | |
| } | |
| } |
| // ```py | ||
| // class tuple: | ||
| // class tuple(Sequence[_T_co]): | ||
| // @overload | ||
| // def __new__(cls) -> tuple[()]: ... | ||
| // @overload | ||
| // def __new__(cls, iterable: Iterable[object]) -> tuple[object, ...]: ... | ||
| // def __new__(cls, iterable: Iterable[_T_co]) -> tuple[_T_co, ...]: ... | ||
| // ``` |
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.
oops, sorry... I guess I baked this bug in a long time ago for you to find now :-)
|
(this will also help with #21499, where I'm currently trying (not very successfully) to do some awkward workarounds due to this missing feature 😆) |
Summary
This solves most of astral-sh/ty#1576. More advanced cases likely require the new constraint solver.