Skip to content

fix infer x: T if x.__class__: type[T] #1163#2475

Open
asukaminato0721 wants to merge 1 commit intofacebook:mainfrom
asukaminato0721:1163
Open

fix infer x: T if x.__class__: type[T] #1163#2475
asukaminato0721 wants to merge 1 commit intofacebook:mainfrom
asukaminato0721:1163

Conversation

@asukaminato0721
Copy link
Contributor

Summary

Fixes #1163

Added a special-case facet narrow so x.__class__ is/== T narrows x like isinstance(x, T).

Test Plan

Added coverage for __class__ attribute narrowing with assert/if and ==.

@meta-cla meta-cla bot added the cla signed label Feb 20, 2026
@asukaminato0721 asukaminato0721 marked this pull request as ready for review February 20, 2026 15:59
Copilot AI review requested due to automatic review settings February 20, 2026 15:59
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 like isinstance checks
  • Added test coverage for __class__ attribute narrowing with assert, if statements, and both is and == 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));
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
}
}
AtomicNarrowOp::IsNot(v) | AtomicNarrowOp::NotEq(v) => {
let right = self.expr_infer(v, errors);
return Some(self.narrow_is_not_instance(base, &right));
}

Copilot uses AI. Check for mistakes.
Comment on lines 104 to 109
assert_type(x, int | str)
if x.__class__ == int:
assert_type(x, int)
"#,
);

Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)
"#,
);

Copilot uses AI. Check for mistakes.
{
match op {
AtomicNarrowOp::Is(v) | AtomicNarrowOp::Eq(v) => {
let right = self.expr_infer(v, errors);
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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);

Copilot uses AI. Check for mistakes.
Comment on lines 498 to 509
// 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> {
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@github-actions

This comment has been minimized.

fix test
@github-actions
Copy link

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]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

infer x: T if x.__class__: type[T]

2 participants