Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions pyrefly/lib/alt/operators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,10 +483,30 @@ impl<'a, Ans: LookupAnswer> AnswersSolver<'a, Ans> {
// Reflected operator implementation: This deviates from the runtime semantics by calling the reflected dunder if the regular dunder call errors.
// At runtime, the reflected dunder is called only if the regular dunder method doesn't exist or if it returns NotImplemented.
// This deviation is necessary, given that the typeshed stubs don't record when NotImplemented is returned
let calls_to_try = [
(&Name::new_static(op.dunder()), lhs, rhs),
(&Name::new_static(op.reflected_dunder()), rhs, lhs),
];
let forward = Name::new_static(op.dunder());
let reflected = Name::new_static(op.reflected_dunder());
let forward_call = (&forward, lhs, rhs);
let reflected_call = (&reflected, rhs, lhs);
// Python data model: when the right operand's type is a *proper* subclass of the
// left operand's type, the reflected dunder is tried first. This lets e.g.
// `int_val & some_IntFlag_member` resolve through `IntFlag.__rand__` (which returns
// the flag type) rather than `int.__and__` (which widens back to `int`). A subclass
// that does not override the reflected dunder inherits it unchanged, and
// `try_binop_calls` still falls back to the forward dunder, so non-overriding
// subclasses are unaffected.
let reflected_first = match (lhs, rhs) {
(Type::ClassType(lhs_cls), Type::ClassType(rhs_cls)) => {
let lhs_obj = lhs_cls.class_object();
let rhs_obj = rhs_cls.class_object();
lhs_obj != rhs_obj && self.has_superclass(rhs_obj, lhs_obj)
}
_ => false,
};
let calls_to_try = if reflected_first {
[reflected_call, forward_call]
} else {
[forward_call, reflected_call]
};
self.try_binop_calls(&calls_to_try, range, errors, &context)
};
self.distribute_over_union(lhs, |lhs| {
Expand Down
21 changes: 21 additions & 0 deletions pyrefly/lib/test/operators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,27 @@ True & ThisClassDoesNotWork(False)
"#,
);

// https://github.com/facebook/pyrefly/issues/3876
testcase!(
test_reflected_dunder_subclass_priority,
r#"
from enum import IntFlag
from typing import assert_type

class Color(IntFlag):
RED = 1
GREEN = 2

def f(x: int, c: Color) -> None:
# `int & Color` invokes `Color.__rand__` at runtime because `Color` is a
# proper subclass of `int` that overrides the reflected dunder, so the result
# keeps the flag type instead of widening to `int`.
assert_type(x & c, Color)
assert_type(x | c, Color)
assert_type(x ^ c, Color)
"#,
);

testcase!(
test_type_of_typevar_equality,
r#"
Expand Down
Loading