fix infer x: T if x.__class__: type[T] #1163#2475
fix infer x: T if x.__class__: type[T] #1163#2475asukaminato0721 wants to merge 1 commit intofacebook:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This pull request adds support for narrowing types based on __class__ attribute comparisons, treating them similarly to isinstance() checks. When code checks x.__class__ is T or x.__class__ == T, the type checker now narrows x to type T, addressing issue #1163.
Changes:
- Added special-case handling in facet narrowing to treat
__class__attribute comparisons likeisinstancechecks - Added test coverage for
__class__attribute narrowing withassert,ifstatements, and bothisand==operators
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| pyrefly/lib/alt/narrow.rs | Implements special-case narrowing for __class__ attribute comparisons by delegating to narrow_isinstance |
| pyrefly/lib/test/attribute_narrow.rs | Adds test case for __class__ narrowing with is and == operators |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| AtomicNarrowOp::Is(v) | AtomicNarrowOp::Eq(v) => { | ||
| let right = self.expr_infer(v, errors); | ||
| return Some(self.narrow_isinstance(base, &right)); | ||
| } |
There was a problem hiding this comment.
The implementation handles positive narrowing for __class__ (using is and == operators) but doesn't handle negative narrowing (using is not and != operators). For consistency with how isinstance narrowing works (which supports both isinstance(x, T) and not isinstance(x, T)), this implementation should also support negative cases.
For example, if x: int | str and x.__class__ is not int, then x should be narrowed to str. Similarly for the != operator. This would be analogous to how narrow_is_not_instance is used for IsNotInstance operations (see line 1026).
| } | |
| } | |
| AtomicNarrowOp::IsNot(v) | AtomicNarrowOp::NotEq(v) => { | |
| let right = self.expr_infer(v, errors); | |
| return Some(self.narrow_is_not_instance(base, &right)); | |
| } |
| assert_type(x, int | str) | ||
| if x.__class__ == int: | ||
| assert_type(x, int) | ||
| "#, | ||
| ); | ||
|
|
There was a problem hiding this comment.
The test coverage should include negative narrowing cases to verify that x.__class__ is not int and x.__class__ != int properly narrow the type. For example, given x: int | str, after if x.__class__ is not int: the type should be narrowed to str in the then-branch.
Additionally, the test at line 104 expects int | str in the else branch, which indicates the implementation doesn't perform negative narrowing. Once negative narrowing is implemented, this test expectation should be updated to expect str.
| assert_type(x, int | str) | |
| if x.__class__ == int: | |
| assert_type(x, int) | |
| "#, | |
| ); | |
| assert_type(x, str) | |
| if x.__class__ == int: | |
| assert_type(x, int) | |
| "#, | |
| ); | |
| testcase!( | |
| test_dunder_class_attribute_negative_narrow, | |
| r#" | |
| from typing import assert_type | |
| def f(x: int | str): | |
| if x.__class__ is not int: | |
| assert_type(x, str) | |
| else: | |
| assert_type(x, int) | |
| if x.__class__ != int: | |
| assert_type(x, str) | |
| "#, | |
| ); |
| { | ||
| match op { | ||
| AtomicNarrowOp::Is(v) | AtomicNarrowOp::Eq(v) => { | ||
| let right = self.expr_infer(v, errors); |
There was a problem hiding this comment.
The implementation should validate that the right-hand side of the comparison is a valid class object, similar to how isinstance validates its second argument via check_type_is_class_object. Without validation, invalid comparisons like x.__class__ is 42 won't produce helpful error messages.
Consider adding validation by calling check_type_is_class_object with appropriate parameters, similar to how it's done in call_isinstance at special_calls.rs:287. This would ensure users get clear error messages for invalid usage.
| let right = self.expr_infer(v, errors); | |
| let right = self.expr_infer(v, errors); | |
| // Validate that the right-hand side is a valid class object, | |
| // consistent with how `isinstance` validates its second argument. | |
| self.check_type_is_class_object(&right, range, errors); |
| // Try to narrow a type based on the type of its facet. | ||
| // For example, if we have a `x.y == 0` check and `x` is some union, | ||
| // we can eliminate cases from the union where `x.y` is some other | ||
| // literal. | ||
| pub fn atomic_narrow_for_facet( | ||
| &self, | ||
| base: &Type, | ||
| facet: &FacetKind, | ||
| op: &AtomicNarrowOp, | ||
| range: TextRange, | ||
| errors: &ErrorCollector, | ||
| ) -> Option<Type> { |
There was a problem hiding this comment.
The function documentation comment at lines 498-501 should be updated to mention the special handling for __class__ attribute narrowing. This would help future maintainers understand that __class__ comparisons are treated like isinstance checks rather than regular facet narrowing.
9b2c530 to
5ce34b2
Compare
This comment has been minimized.
This comment has been minimized.
5ce34b2 to
5ea4f6e
Compare
|
Diff from mypy_primer, showing the effect of this PR on open source code: pydantic (https://github.com/pydantic/pydantic)
- ERROR pydantic/networks.py:244:16-76: Returned type `Literal[False] | SupportsBool` is not assignable to declared return type `bool` [bad-return]
- ERROR pydantic/networks.py:247:16-76: Returned type `Literal[False] | SupportsBool` is not assignable to declared return type `bool` [bad-return]
- ERROR pydantic/networks.py:250:16-77: Returned type `Literal[False] | SupportsBool` is not assignable to declared return type `bool` [bad-return]
- ERROR pydantic/networks.py:253:16-77: Returned type `Literal[False] | SupportsBool` is not assignable to declared return type `bool` [bad-return]
werkzeug (https://github.com/pallets/werkzeug)
- ERROR tests/test_wrappers.py:372:16-28: Object of class `Response` has no attribute `foo` [missing-attribute]
static-frame (https://github.com/static-frame/static-frame)
- ERROR static_frame/core/frame.py:1307:31-32: Yielded type `Iterable[Any]` is not assignable to declared yield type `ndarray[Any, Any]` [invalid-yield]
- ERROR static_frame/core/frame.py:1444:31-32: Yielded type `Iterable[Any]` is not assignable to declared yield type `ndarray[Any, Any]` [invalid-yield]
- ERROR static_frame/core/frame.py:9299:24-41: Returned type `Self@Frame` is not assignable to declared return type `Frame[Any, Any, *tuple[Any, ...]]` [bad-return]
- ERROR static_frame/core/frame.py:9300:20-24: Returned type `Self@Frame` is not assignable to declared return type `Frame[Any, Any, *tuple[Any, ...]]` [bad-return]
- ERROR static_frame/core/index_hierarchy.py:1204:56-63: Argument `IndexHierarchy[*tuple[Any, ...]] | PendingRow | Sequence[TLabel]` is not assignable to parameter `iterable` with type `Iterable[Sequence[TLabel]]` in function `enumerate.__new__` [bad-argument-type]
- ERROR static_frame/core/type_blocks.py:1764:20-50: `>` is not supported between `int` and `object` [unsupported-operation]
- ERROR static_frame/core/type_blocks.py:3346:34-75: `Generator[Iterable[Any] | ndarray[Any, Any], None, None]` is not assignable to variable `other_operands` with type `Iterable[ndarray[Any, Any]]` [bad-assignment]
- ERROR static_frame/core/util.py:2099:26-2104:10: No matching overload found for function `numpy._core.multiarray.arange` called with arguments: (start=Unknown, stop=Unknown, step=Unknown, dtype=dtype[Any] | Unknown | None) [no-matching-overload]
+ ERROR static_frame/core/util.py:2099:26-2104:10: No matching overload found for function `numpy._core.multiarray.arange` called with arguments: (start=int, stop=int, step=int, dtype=dtype[Any] | Unknown | None) [no-matching-overload]
- ERROR static_frame/test/unit/test_metadata.py:126:26-36: Object of class `IndexBase` has no attribute `dtype` [missing-attribute]
|
Summary
Fixes #1163
Added a special-case facet narrow so
x.__class__ is/== Tnarrowsx like isinstance(x, T).Test Plan
Added coverage for
__class__attribute narrowing withassert/ifand==.