Fix type narrowing for type(x) expressions with type[T] parameters and reverse comparisons - #11619
Fix type narrowing for type(x) expressions with type[T] parameters and reverse comparisons#11619Henry Su (hsusul) wants to merge 3 commits into
Conversation
…d reverse comparisons - Support type(x) calls on either left or right hand side of binary comparison operators. - Unwrap instantiable class types from type[T] expressions when evaluating type(x) type guards. - Correctly eliminate disjoint subtypes in positive type(x) narrowing. - Add comprehensive regression test suite in typeIs6.py.
|
🔒 Automated review in progress — Stella Huang (@StellaHuang95) is auto-reviewing this PR. |
|
Negative |
|
The negative narrowing change can incorrectly eliminate a valid superclass subtype when the compared class is a final subclass, producing unsound results. |
|
The negative narrowing change can incorrectly eliminate a non-final base class when the compared class is a final subclass, producing an unsound narrowed type. |
…ainst final subclasses - Restore ClassType.isFinal(instantiableSubtype) check in negative type(x) narrowing to prevent unsoundly eliminating non-final superclass subtypes when comparing against final subclasses. - Add regression test func8 in typeIs6.py covering non-final Base class compared with @Final FinalSub.
|
Thank you for the review and sharp catch Stella Huang (@StellaHuang95)! I have updated the negative narrowing logic in I have pushed the fix and added a regression test ( class Base: pass
@final
class FinalSub(Base): pass
def func8(x: Base):
if type(x) is not FinalSub:
reveal_type(x, expected_text="Base")
else:
reveal_type(x, expected_text="FinalSub") |
|
GitHub cannot anchor PR review comments to unchanged lines in the diff. Falling back to a general PR comment for packages/pyright-internal/src/analyzer/typeGuards.ts:L2626.
|
| }; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Warning · Non-blocking recommendation
The blanket object exclusion also changes direct type(x) is object checks from narrowing the positive branch to Never to performing no narrowing. It also disables the entire callback for type[int] | type[object], losing the useful int narrowing. Please scope the exclusion to the unwrapped type[object] case, or add tests documenting that this broader precision loss is intentional.
Stella Huang (StellaHuang95)
left a comment
There was a problem hiding this comment.
Approved via Review Center.
This comment has been minimized.
This comment has been minimized.
|
I took a closer look at the The main issue is that the new logic converts the argument of
I recommend preserving subclass inclusion for operands typed |
Mypy_primer differences should be addressed first
…ards - Preserve includeSubclasses = true when unwrapping operands typed type[T] (e.g. cls: type[Index] or type(other)) to avoid unsoundly narrowing type(self) is not type(other) when self is a subclass of other (resolving Spark MultiIndex.symmetric_difference regression). - Reserve exact-class treatment (includeSubclasses = false) for direct class expressions (e.g. Base, int) and @Final classes. - Add regression tests test_spark_regression and test_direct_class_vs_type_param in typeIs6.py.
|
Thank you for the detailed I have updated the binary type guard unwrapping logic in
Added Tests in
|
|
Diff from mypy_primer, showing the effect of this PR on open source code: koda-validate (https://github.com/keithasaurus/koda-validate)
+ .../projects/koda-validate/koda_validate/generic.py:161:38 - error: Argument of type "bool* | bytes* | int* | Decimal* | str* | float* | date* | datetime* | UUID* | Any" cannot be assigned to parameter "val" of type "ExactMatchT@EqualsValidator" in function "__call__"
+ Type "bool* | bytes* | int* | Decimal* | str* | float* | date* | datetime* | UUID* | Any" is not assignable to type "ExactMatchT@EqualsValidator" (reportArgumentType)
+ .../projects/koda-validate/koda_validate/generic.py:163:31 - error: Argument of type "bool* | bytes* | int* | Decimal* | str* | float* | date* | datetime* | UUID* | Any" cannot be assigned to parameter "val" of type "ExactMatchT@EqualsValidator" in function "__call__"
+ Type "bool* | bytes* | int* | Decimal* | str* | float* | date* | datetime* | UUID* | Any" is not assignable to type "ExactMatchT@EqualsValidator" (reportArgumentType)
+ .../projects/koda-validate/koda_validate/generic.py:164:24 - error: Type "tuple[Literal[True], bool* | bytes* | int* | Decimal* | str* | float* | date* | datetime* | UUID* | Any]" is not assignable to return type "_ResultTuple[ExactMatchT@EqualsValidator]"
+ Type "tuple[Literal[True], bool* | bytes* | int* | Decimal* | str* | float* | date* | datetime* | UUID* | Any]" is not assignable to type "_ResultTuple[ExactMatchT@EqualsValidator]"
+ "tuple[Literal[True], bool* | bytes* | int* | Decimal* | str* | float* | date* | datetime* | UUID* | Any]" is not assignable to "tuple[Literal[True], ExactMatchT@EqualsValidator]"
+ Tuple entry 2 is incorrect type
+ Type "bool* | bytes* | int* | Decimal* | str* | float* | date* | datetime* | UUID* | Any" is not assignable to type "ExactMatchT@EqualsValidator"
+ "tuple[Literal[True], bool* | bytes* | int* | Decimal* | str* | float* | date* | datetime* | UUID* | Any]" is not assignable to "tuple[Literal[False], Invalid]"
+ Tuple entry 1 is incorrect type
+ "Literal[True]" is not assignable to type "Literal[False]" (reportReturnType)
- 95 errors, 0 warnings, 0 informations
+ 98 errors, 0 warnings, 0 informations
spark (https://github.com/apache/spark)
+ .../projects/spark/python/pyspark/pandas/indexes/multi.py:817:45 - error: "Never" is not iterable (reportGeneralTypeIssues)
+ .../projects/spark/python/pyspark/pandas/indexes/multi.py:822:55 - error: "Never" is not iterable (reportGeneralTypeIssues)
- 33760 errors, 966 warnings, 0 informations
+ 33762 errors, 966 warnings, 0 informations
yarl (https://github.com/aio-libs/yarl)
- .../projects/yarl/yarl/_url.py:447:20 - error: Type "str* | SplitResult* | URL* | UndefinedType*" is not assignable to return type "URL"
- Type "str* | SplitResult* | URL* | UndefinedType*" is not assignable to type "URL"
- "SplitResult*" is not assignable to "URL" (reportReturnType)
- 43 errors, 5 warnings, 0 informations
+ 42 errors, 5 warnings, 0 informations
|
| }; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Warning · Non-blocking recommendation
The object exclusion now also rejects the pre-existing direct type(x) is object form. Previously it produced an impossible positive branch (Never); now isClassType becomes false and no narrowing callback is returned. It also discards useful alternatives in unions such as type[int] | type[object]. Please exclude only the parameterized type[object] case, or skip that subtype rather than disabling the entire callback.
| const otherResult = evaluator.getTypeOfExpression(otherExpr); | ||
| const classTypes: ClassType[] = []; | ||
| let isClassType = true; | ||
|
|
There was a problem hiding this comment.
Info · Optional note
When both operands are calls, the left call is selected before checking whether its argument matches the reference. As a result, querying narrowing for x in type(y) is type(x) stops after type(y) fails the match and never considers the RHS type(x). Please fall through to the RHS call when the selected call does not match the reference.
Stella Huang (StellaHuang95)
left a comment
There was a problem hiding this comment.
Approved via Review Center.
Rich Chiodo (rchiodo)
left a comment
There was a problem hiding this comment.
Approved via Review Center.
Summary
Fixes a type narrowing issue where binary comparison type guards involving
type(x)expressions failed to narrow types when:type(x)appeared on the right-hand side of the comparison (e.g.if cls is type(x):orif int is type(x):).type[T](e.g.cls: type[int]).Root Cause
type(x)previously checked onlytestExpression.d.leftExpr.nodeType === ParseNodeType.Call. Whentype(x)was on the RHS, Pyright skipped generating a type guard callback.type(x)against a variableclsof typetype[T],getTypeOfExpression(cls)returns aClassTypeinstance oftype. Pyright checkedisInstantiableClass(expandedSubtype), which returnedfalsefortype[T]instances, preventingclassTypesfrom being populated.narrowTypeForTypeIsdid not eliminate disjoint subtypes when comparing against class types derived fromtype[T].Changes
type(x)on eitherleftExprorrightExprofis,is not,==, and!=binary comparison operators.type[T]instances (ClassTypebuilt-intypewithtypeArgs) to instantiable class types (convertToInstantiable(typeArgs[0], false)).narrowTypeForTypeIsto check subclass overlap and eliminate disjoint subtypes.type[object]operands to preserve correct behavior when comparing against genericobjectinstances.typeIs6.pycovering alltype(x)comparison forms,type[T]parameters,@finalclasses, and union types.