[ty] Preserve exact class objects during identity narrowing#26117
Conversation
Typing conformance resultsNo changes detected ✅Current numbersThe percentage of diagnostics emitted that were expected errors held steady at 94.37%. The percentage of expected errors that received a diagnostic held steady at 89.00%. The number of fully passing files held steady at 94/134. |
Memory usage reportMemory usage unchanged ✅ |
|
| Lint rule | Added | Removed | Changed |
|---|---|---|---|
invalid-assignment |
0 | 0 | 1 |
unused-type-ignore-comment |
0 | 1 | 0 |
| Total | 0 | 1 | 1 |
Flaky changes detected. This PR summary excludes flaky changes; see the HTML report for details.
Raw diff:
steam.py (https://github.com/Gobot1234/steam.py)
- steam/ext/commands/commands.py:826:11 error[invalid-assignment] Object of type `<class 'Command'>` is not assignable to `type[C@command]`
+ steam/ext/commands/commands.py:826:11 error[invalid-assignment] Object of type `(type[C@command] & ~AlwaysFalsy) | <class 'Command'>` is not assignable to `type[C@command]`
sympy (https://github.com/sympy/sympy)
- sympy/core/function.py:451:49 warning[unused-type-ignore-comment] Unused blanket `type: ignore` directive|
|
||
| def narrow[T: (Y, Z)](klass: type[T]) -> None: | ||
| if klass is Y: | ||
| reveal_type(klass) # revealed: type[T@narrow] & <class 'Y'> |
There was a problem hiding this comment.
On main, reveals type[T@narrow]
| @final | ||
| class GenericFinal[T]: | ||
| @classmethod | ||
| def class_object(cls) -> "TypeOf[GenericFinal[T]]": |
There was a problem hiding this comment.
Without the final "special-case", this raises invalid-return-type
|
Passing to @carljm since he reviewed my prior attempt at this... |
| from ty_extensions import TypeOf | ||
|
|
||
| @final | ||
| class Final: ... |
There was a problem hiding this comment.
Nit: Avoid using the name Final, too easily confused with typing.Final.
|
|
||
| ```py | ||
| from typing import final | ||
| from ty_extensions import TypeOf |
There was a problem hiding this comment.
Is there a way to exercise what this test is supposed to without using TypeOf?
|
|
||
| ```py | ||
| from typing import final | ||
| from ty_extensions import TypeOf |
There was a problem hiding this comment.
Also here, is there a more realistic way to demonstrate the motivating case here without TypeOf?
| source_i | ||
| .typevar(db) | ||
| .upper_bound(db) | ||
| .and_then(|bound| SubclassOfType::try_from_instance(db, bound)) |
There was a problem hiding this comment.
We should also unwrap type aliases here, and add a test similar to this:
@final
class C: ...
type Alias = C
def accepts_exact(cls: TypeOf[C]) -> None: ...
def bounded[T: Alias](cls: type[T]) -> None:
accepts_exact(cls)82ff548 to
c2689eb
Compare
Summary
Prior to this change, when narrowing
klass is YforT: (Y, Z), whereZextendsY, we incorrectly simplifiedtype[T] & <class 'Y'>totype[T].Both
YandZinstances are subtypes ofY, but only the class objectYsatisfiesklass is Y. Using instance subtyping for the exact class object discarded that information, causingYitself to widen totype[T]andY()to be checked againstZ's constructor. We now exclude exact class literals and specialized generic aliases from that check.We still retain this behavior when a type variable’s upper bound normalizes to the exact target class object. This can only happen for a final class, so that class object is the type variable’s only possible specialization. (This removed ~10 ecosystem false-positives.)
Closes astral-sh/ty#3074.