diff --git a/crates/pyrefly_bundled/third_party/stubs/sympy-stubs/polys/matrices/domainmatrix.pyi b/crates/pyrefly_bundled/third_party/stubs/sympy-stubs/polys/matrices/domainmatrix.pyi index ba7c1242ad..84f43f14da 100644 --- a/crates/pyrefly_bundled/third_party/stubs/sympy-stubs/polys/matrices/domainmatrix.pyi +++ b/crates/pyrefly_bundled/third_party/stubs/sympy-stubs/polys/matrices/domainmatrix.pyi @@ -62,8 +62,8 @@ class DomainMatrix: def hstack(A, *B) -> DomainMatrix: ... def vstack(A, *B) -> DomainMatrix: ... def applyfunc(self, func, domain=...) -> Self: ... - def __add__(A, B) -> NotImplementedType: ... - def __sub__(A, B) -> NotImplementedType: ... + def __add__(A, B) -> Self | NotImplementedType: ... + def __sub__(A, B) -> Self | NotImplementedType: ... def __neg__(A) -> Self: ... def __mul__(A, B) -> DomainMatrix | Self | NotImplementedType: ... def __rmul__(A, B) -> DomainMatrix | Self | NotImplementedType: ... @@ -77,7 +77,7 @@ class DomainMatrix: def scalarmul(A, lamda) -> DomainMatrix | Self: ... def rscalarmul(A, lamda) -> DomainMatrix | Self: ... def mul_elementwise(A, B) -> Self: ... - def __truediv__(A, lamda) -> NotImplementedType: ... + def __truediv__(A, lamda) -> Self | NotImplementedType: ... def pow(A, n) -> Self: ... def scc(self) -> list[Any]: ... def rref(self) -> tuple[Self, tuple[Any, ...]]: ... diff --git a/crates/pyrefly_bundled/third_party/stubs/sympy-stubs/polys/rings.pyi b/crates/pyrefly_bundled/third_party/stubs/sympy-stubs/polys/rings.pyi index a45a038c3d..5553b17c13 100644 --- a/crates/pyrefly_bundled/third_party/stubs/sympy-stubs/polys/rings.pyi +++ b/crates/pyrefly_bundled/third_party/stubs/sympy-stubs/polys/rings.pyi @@ -132,17 +132,17 @@ class PolyElement(DomainElement, DefaultPrinting, CantSympify, dict): def __add__(p1, p2) -> Self | PolyElement | NotImplementedType: ... def __radd__(p1, n) -> Self | NotImplementedType: ... def __sub__(p1, p2) -> Self | NotImplementedType: ... - def __rsub__(p1, n) -> NotImplementedType: ... - def __mul__(p1, p2) -> NotImplementedType: ... - def __rmul__(p1, p2) -> NotImplementedType: ... + def __rsub__(p1, n) -> Self | NotImplementedType: ... + def __mul__(p1, p2) -> Self | PolyElement | NotImplementedType: ... + def __rmul__(p1, p2) -> Self | NotImplementedType: ... def __pow__(self, n) -> Self: ... def square(self): ... def __divmod__(p1, p2) -> tuple[Any, Any] | tuple[list[Any], Any] | NotImplementedType | tuple[Self, Self]: ... - def __rdivmod__(p1, p2) -> NotImplementedType: ... + def __rdivmod__(p1, p2) -> tuple[Self, Self] | NotImplementedType: ... def __mod__(p1, p2) -> NotImplementedType | Self: ... - def __rmod__(p1, p2) -> NotImplementedType: ... + def __rmod__(p1, p2) -> Self | NotImplementedType: ... def __truediv__(p1, p2) -> list[Any] | NotImplementedType | Self: ... - def __rtruediv__(p1, p2) -> NotImplementedType: ... + def __rtruediv__(p1, p2) -> Self | NotImplementedType: ... __floordiv__ = ... __rfloordiv__ = ... diff --git a/crates/pyrefly_python/src/dunder.rs b/crates/pyrefly_python/src/dunder.rs index 6a298ff392..7c8d6c4c11 100644 --- a/crates/pyrefly_python/src/dunder.rs +++ b/crates/pyrefly_python/src/dunder.rs @@ -202,8 +202,8 @@ pub fn rich_comparison_fallback(op: CmpOp) -> Option { let name = match op { CmpOp::Lt => GT, CmpOp::LtE => GE, - CmpOp::Eq => NE, - CmpOp::NotEq => EQ, + CmpOp::Eq => EQ, + CmpOp::NotEq => NE, CmpOp::Gt => LT, CmpOp::GtE => LE, _ => return None, diff --git a/pyrefly/lib/alt/operators.rs b/pyrefly/lib/alt/operators.rs index 897b57b2a3..70ff7d9bec 100644 --- a/pyrefly/lib/alt/operators.rs +++ b/pyrefly/lib/alt/operators.rs @@ -11,6 +11,7 @@ use pyrefly_python::dunder; use pyrefly_types::dimension::Int; use pyrefly_types::dimension::canonicalize; use pyrefly_types::dimension::int_type_is_provably_nonnegative; +use pyrefly_types::function::BodyKind; use pyrefly_types::lit_int::LitInt; use pyrefly_types::literal::LitStyle; use pyrefly_types::quantified::Quantified; @@ -48,6 +49,7 @@ use crate::module::typeshed::typeshed; use crate::types::class::Class; use crate::types::literal::Lit; use crate::types::tuple::Tuple; +use crate::types::types::BoundMethodType; use crate::types::types::Type; #[derive(Clone, Copy, PartialEq, Eq)] @@ -58,6 +60,30 @@ enum EqualityCompatibilityGroup { Str, } +enum BinaryOperatorOrder { + ForwardOnly, + ForwardFirst, + ReflectedFirst, +} + +/// A value includes `Never`, which means that execution does not return. +enum OperatorReturn { + Value(Type), + Declined, + ValueOrDecline(Type), +} + +/// Failed signatures retain their diagnostics and recovery type until dispatch completes. +enum OperatorCallOutcome { + Missing, + SignatureError { + callee_errors: ErrorCollector, + call_errors: ErrorCollector, + ret: Type, + }, + Returned(OperatorReturn), +} + impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { fn callable_dunder_helper( &self, @@ -225,49 +251,140 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { Some(result_ty) } + /// Interpret a successful call without selecting another operator or a completion rule. + fn interpret_operator_return(&self, ret: Type, inferred_placeholder: bool) -> OperatorReturn { + let is_not_implemented = |ty: &Type| matches!(ty, Type::ClassType(cls) if cls.is_builtin("_NotImplementedType") || cls.has_qname("types", "NotImplementedType")); + match ret { + ret if is_not_implemented(&ret) => { + if inferred_placeholder { + // A wrapper can inherit a sentinel inferred from a base helper, even when + // a subclass overrides that helper. Its concrete result is unknown. + OperatorReturn::Value(self.heap.mk_any_implicit()) + } else { + OperatorReturn::Declined + } + } + Type::Union(mut union) => { + if let Some(index) = union.members.iter().position(is_not_implemented) { + union.members.remove(index); + // Mixed inferred returns retain their concrete types. Identifying returns + // delegated to an overridden helper requires per-return provenance. + OperatorReturn::ValueOrDecline(self.unions(union.members)) + } else { + OperatorReturn::Value(Type::Union(union)) + } + } + ret => OperatorReturn::Value(ret), + } + } + + /// Report successful-call diagnostics immediately, including methods that decline. + fn call_operator( + &self, + dunder: &Name, + target: &Type, + arg: &Type, + range: TextRange, + errors: &ErrorCollector, + context: &dyn Fn() -> ErrorContext, + ) -> OperatorCallOutcome { + let Some(method) = self.type_of_magic_dunder_attr( + target, + dunder, + range, + errors, + Some(&context), + "Expr::binop_infer", + // Operator lookup must not use __getattr__ or __getattribute__. + false, + ) else { + return OperatorCallOutcome::Missing; + }; + let inferred_placeholder = match &method { + Type::Overload(_) => false, + Type::BoundMethod(method) if matches!(method.func, BoundMethodType::Overload(_)) => { + false + } + _ => method.toplevel_func_metadata().is_some_and(|metadata| { + metadata.flags.is_return_inferred + && metadata.flags.body_kind != BodyKind::ReturnNotImplemented + }), + }; + let callee_errors = self.error_collector(); + let call_errors = self.error_collector(); + let ret = self.callable_dunder_helper( + method, + range, + &callee_errors, + &call_errors, + &context, + dunder, + arg, + ); + // Soft errors must not reject an otherwise valid signature. + if call_errors.has_hard() { + OperatorCallOutcome::SignatureError { + callee_errors, + call_errors, + ret, + } + } else { + errors.extend(callee_errors); + OperatorCallOutcome::Returned(self.interpret_operator_return(ret, inferred_placeholder)) + } + } + + /// Callers distribute operand unions and supply the existing method order. + /// A definite value ends dispatch. Otherwise, completion uses `unresolved_result` or + /// recovers the first signature error when no concrete result is possible. fn try_binop_calls( &self, calls: &[(&Name, &Type, &Type)], range: TextRange, errors: &ErrorCollector, context: &dyn Fn() -> ErrorContext, + unresolved_result: Option, ) -> Type { - let mut first_call = None; + let mut first_error = None; + let mut concrete_result = None; for (dunder, target, arg) in calls { - let method_type_dunder = self.type_of_magic_dunder_attr( - target, - dunder, - range, - errors, - Some(&context), - "Expr::binop_infer", - // Magic method lookup for operators should ignore __getattr__/__getattribute__. - false, - ); - let Some(method_type_dunder) = method_type_dunder else { - continue; - }; - let callee_errors = self.error_collector(); - let call_errors = self.error_collector(); - let ret = self.callable_dunder_helper( - method_type_dunder, - range, - &callee_errors, - &call_errors, - &context, - dunder, - arg, - ); - // Soft errors (e.g. unknown-argument-type) must not reject an otherwise - // valid dunder call, so gate on hard errors only. - if !call_errors.has_hard() { - errors.extend(callee_errors); - return ret; - } else if first_call.is_none() { - first_call = Some((callee_errors, call_errors, ret)); + match self.call_operator(dunder, target, arg, range, errors, context) { + OperatorCallOutcome::Missing + | OperatorCallOutcome::Returned(OperatorReturn::Declined) => {} + OperatorCallOutcome::SignatureError { + callee_errors, + call_errors, + ret, + } => { + if first_error.is_none() { + first_error = Some((callee_errors, call_errors, ret)); + } + } + OperatorCallOutcome::Returned(OperatorReturn::Value(ret)) => { + return match concrete_result { + Some(previous) => self.union(previous, ret), + None => ret, + }; + } + OperatorCallOutcome::Returned(OperatorReturn::ValueOrDecline(ret)) => { + concrete_result = Some(match concrete_result { + Some(previous) => self.union(previous, ret), + None => ret, + }); + } } } - if let Some((callee_errors, call_errors, ret)) = first_call { + if first_error.is_none() + && let Some(fallback) = unresolved_result + { + return match concrete_result { + Some(previous) => self.union(previous, fallback), + None => fallback, + }; + } + if let Some(ret) = concrete_result { + ret + } else if let Some((callee_errors, call_errors, ret)) = first_error { errors.extend(callee_errors); errors.extend(call_errors); ret @@ -607,6 +724,22 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { } } + /// Equal classes cannot use reflected arithmetic. Subclasses retain the existing + /// priority approximation, which does not compare reflected method identities. + fn binary_operator_order(&self, lhs: &Type, rhs: &Type) -> BinaryOperatorOrder { + if let (Type::ClassType(lhs_cls), Type::ClassType(rhs_cls)) = (lhs, rhs) { + let lhs_obj = lhs_cls.class_object(); + let rhs_obj = rhs_cls.class_object(); + if lhs_obj == rhs_obj { + return BinaryOperatorOrder::ForwardOnly; + } + if self.has_superclass(rhs_obj, lhs_obj) { + return BinaryOperatorOrder::ReflectedFirst; + } + } + BinaryOperatorOrder::ForwardFirst + } + fn binop_types(&self, x: &ExprBinOp, lhs: &Type, rhs: &Type, errors: &ErrorCollector) -> Type { if let Some(result) = Self::try_literal_int_binop(x.op, lhs, rhs) { return result; @@ -631,27 +764,16 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { 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) + let mut calls = [forward_call, reflected_call]; + let calls_to_try = match self.binary_operator_order(lhs, rhs) { + BinaryOperatorOrder::ForwardOnly => &calls[..1], + BinaryOperatorOrder::ForwardFirst => &calls, + BinaryOperatorOrder::ReflectedFirst => { + calls.swap(0, 1); + &calls } - _ => 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.try_binop_calls(calls_to_try, range, errors, &context, None) }; self.distribute_over_union(lhs, |lhs| { self.distribute_over_union(rhs, |rhs| { @@ -718,22 +840,7 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { self.heap.mk_class_type(self.stdlib.int().clone()) } } - _ => { - let context = || { - ErrorContext::BinaryOp( - x.op.as_str().to_owned(), - self.for_display(lhs.clone()), - self.for_display(rhs.clone()), - left_range, - right_range, - ) - }; - let calls = [ - (&Name::new_static(x.op.dunder()), lhs, rhs), - (&Name::new_static(x.op.reflected_dunder()), rhs, lhs), - ]; - self.try_binop_calls(&calls, x.range, errors, &context) - } + _ => binop_call(x.op, lhs, rhs, x.range), } } else { self.on_quantifieds(lhs, rhs, &|left, right| { @@ -763,12 +870,21 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { value_range, ) }; - let calls_to_try = [ + let mut calls = [ (&Name::new_static(op.in_place_dunder()), lhs, rhs), (&Name::new_static(op.dunder()), lhs, rhs), (&Name::new_static(op.reflected_dunder()), rhs, lhs), ]; - self.try_binop_calls(&calls_to_try, range, errors, &context) + // The inplace method precedes ordinary binary dispatch, even for subclasses. + let calls_to_try = match self.binary_operator_order(lhs, rhs) { + BinaryOperatorOrder::ForwardOnly => &calls[..2], + BinaryOperatorOrder::ForwardFirst => &calls, + BinaryOperatorOrder::ReflectedFirst => { + calls.swap(1, 2); + &calls + } + }; + self.try_binop_calls(calls_to_try, range, errors, &context, None) }; let base = self.expr_infer(&x.target, errors); let rhs = if (x.op == Operator::BitOr @@ -1027,8 +1143,15 @@ impl<'ctx, 'answer, Ans: LookupAnswer> AnswersSolver<'ctx, 'answer, Ans> { (&dunder::rich_comparison_dunder(op).unwrap(), left, right), (&dunder::rich_comparison_fallback(op).unwrap(), right, left), ]; - let ret = - self.try_binop_calls(&calls_to_try, x.range, errors, &context); + let identity_fallback = matches!(op, CmpOp::Eq | CmpOp::NotEq) + .then(|| self.heap.mk_class_type(self.stdlib.bool().clone())); + let ret = self.try_binop_calls( + &calls_to_try, + x.range, + errors, + &context, + identity_fallback, + ); if ret.is_error() { self.heap.mk_class_type(self.stdlib.bool().clone()) } else { diff --git a/pyrefly/lib/test/operators.rs b/pyrefly/lib/test/operators.rs index 9b32a6b50b..89447bb6b4 100644 --- a/pyrefly/lib/test/operators.rs +++ b/pyrefly/lib/test/operators.rs @@ -706,6 +706,473 @@ assert_type(a, A) "#, ); +#[cfg(feature = "third-party-stubs")] +testcase!( + test_bundled_sympy_operators, + r#" +from types import NotImplementedType +from typing import TYPE_CHECKING, assert_type + +if TYPE_CHECKING: + from sympy.polys.matrices.domainmatrix import DomainMatrix + from sympy.polys.rings import PolyElement + +def matrices(left: DomainMatrix, right: DomainMatrix) -> None: + assert_type(left + right, DomainMatrix) + assert_type(left - right, DomainMatrix) + assert_type(left / 2, DomainMatrix) + assert_type(left.__add__(right), DomainMatrix | NotImplementedType) + assert_type(left.__sub__(right), DomainMatrix | NotImplementedType) + assert_type(left.__truediv__(2), DomainMatrix | NotImplementedType) + +def polynomials(left: PolyElement, right: PolyElement) -> None: + assert_type(left * right, PolyElement) + assert_type(left * 2, PolyElement) + assert_type(2 * left, PolyElement) + assert_type(2 - left, PolyElement) + assert_type(divmod(2, left), tuple[PolyElement, PolyElement] | NotImplementedType) + assert_type(2 % left, PolyElement) + assert_type(0 / left, PolyElement) + assert_type(left.__mul__(right), PolyElement | NotImplementedType) + assert_type(left.__rmul__(2), PolyElement | NotImplementedType) + assert_type(left.__rsub__(2), PolyElement | NotImplementedType) + assert_type(left.__rdivmod__(2), tuple[PolyElement, PolyElement] | NotImplementedType) + assert_type(left.__rmod__(2), PolyElement | NotImplementedType) + assert_type(left.__rtruediv__(0), PolyElement | NotImplementedType) +"#, +); + +testcase!( + test_not_implemented_same_class_arithmetic, + r#" +from types import NotImplementedType +from typing import assert_type + +class Same: + def __add__(self, other: object) -> NotImplementedType: + return NotImplemented + + def __radd__(self, other: object) -> str: + return "reflected" + +Same() + Same() # E: Cannot find `__add__` + +def inplace() -> None: + value = Same() + value += Same() # E: Cannot find `__iadd__` or `__add__` + +class SameInplace(Same): + def __iadd__(self, other: object) -> NotImplementedType: + return NotImplemented + +def declined_inplace() -> None: + value = SameInplace() + value += SameInplace() # E: Cannot find `__iadd__` or `__add__` + +class Partial: + def __add__(self, other: object) -> int | NotImplementedType: + return NotImplemented + + def __radd__(self, other: object) -> str: + return "reflected" + +assert_type(Partial() + Partial(), int) + +def partial_inplace() -> None: + value = Partial() + value += Partial() + assert_type(value, int) + +class ReflectedOnly: + def __radd__(self, other: object) -> str: + return "reflected" + +ReflectedOnly() + ReflectedOnly() # E: Cannot find `__add__` +"#, +); + +testcase!( + test_inplace_reflected_subclass_priority, + r#" +from types import NotImplementedType +from typing import Never, assert_type + +class Base: + def __add__(self, other: object) -> int: + return 1 + +class Child(Base): + def __radd__(self, other: object) -> str: + return "reflected" + +def absent_inplace() -> None: + value = Base() + value += Child() + assert_type(value, str) + +class BadSignature(Base): + def __iadd__(self, other: Never) -> bytes: + return b"" + +class BadSignatureChild(BadSignature): + def __radd__(self, other: object) -> str: + return "reflected" + +def signature_recovery() -> None: + value = BadSignature() + value += BadSignatureChild() + assert_type(value, str) + +class Declining(Base): + def __iadd__(self, other: object) -> NotImplementedType: + return NotImplemented + +class DecliningChild(Declining): + def __radd__(self, other: object) -> str: + return "reflected" + +def declined_inplace() -> None: + value = Declining() + value += DecliningChild() + assert_type(value, str) + +class Partial(Base): + def __iadd__(self, other: object) -> bytes | NotImplementedType: + return NotImplemented + +class PartialChild(Partial): + def __radd__(self, other: object) -> str: + return "reflected" + +def partial_inplace() -> None: + value = Partial() + value += PartialChild() + assert_type(value, bytes | str) + +class Successful(Base): + def __iadd__(self, other: object) -> float: + return 1.0 + +class SuccessfulChild(Successful): + def __radd__(self, other: object) -> str: + return "reflected" + +def successful_inplace() -> None: + value = Successful() + value += SuccessfulChild() + assert_type(value, float) +"#, +); + +testcase!( + test_not_implemented_binary_returns, + r#" +from types import NotImplementedType +from typing import Never, NoReturn, assert_type + +class Declines: + def __add__(self, other: object) -> NotImplementedType: + return NotImplemented + +class Partial: + def __add__(self, other: object) -> int | NotImplementedType: + return NotImplemented + +class Right: + def __radd__(self, other: object) -> str: + return "" + +class Raises: + def __add__(self, other: object) -> Never: + raise RuntimeError + +class LegacyRaises: + def __add__(self, other: object) -> NoReturn: + raise RuntimeError + +assert_type(Declines() + Right(), str) +assert_type(Partial() + Right(), int | str) +assert_type(Partial() + object(), int) +Declines() + object() # E: Cannot find `__add__` or `__radd__` + +class BadSignature: + def __add__(self, other: Never) -> bytes: + return b"" + +assert_type(BadSignature() + Right(), str) +BadSignature() + Declines() # E: `Declines` is not assignable to parameter `other` with type `Never` + +def never_with_reflected() -> None: + assert_type(Raises() + Right(), Never) + +def never_without_reflected() -> None: + assert_type(Raises() + object(), Never) + +def no_return() -> None: + assert_type(LegacyRaises() + Right(), Never) + "#, +); + +testcase!( + test_not_implemented_operand_unions, + r#" +from types import NotImplementedType +from typing import assert_type, overload + +class Accepted: + def __radd__(self, other: object) -> bytes: + return b"" + +class Rejected: + def __radd__(self, other: object) -> str: + return "" + +class Left: + @overload + def __add__(self, other: Accepted) -> int: ... + @overload + def __add__(self, other: Rejected) -> NotImplementedType: ... + def __add__(self, other: Accepted | Rejected) -> int | NotImplementedType: + return 1 if isinstance(other, Accepted) else NotImplemented + +def right_union(other: Accepted | Rejected) -> None: + assert_type(Left() + other, int | str) + +class DecliningLeft: + def __add__(self, other: object) -> NotImplementedType: + return NotImplemented + +def left_union(left: Left | DecliningLeft, right: Accepted | Rejected) -> None: + assert_type(left + right, int | bytes | str) + +class SuccessfulLeft: + def __add__(self, other: object) -> int: + return 1 + +class SelectiveRight: + @overload + def __radd__(self, other: SuccessfulLeft) -> bytes: ... + @overload + def __radd__(self, other: DecliningLeft) -> str: ... + def __radd__(self, other: SuccessfulLeft | DecliningLeft) -> bytes | str: + return "" + +def only_declining_left(left: SuccessfulLeft | DecliningLeft) -> None: + assert_type(left + SelectiveRight(), int | str) + "#, +); + +testcase!( + test_not_implemented_inferred_wrappers, + r#" +from types import NotImplementedType +from typing import Any, Self, assert_type, overload + +class Base: + def apply(self, other: object): + return NotImplemented + + def __add__(self, other: object): + return self.apply(other) + + def add_self(self, other: object) -> None: + assert_type(self + other, Any) + + def add_explicit_self(self: Self, other: object) -> None: + assert_type(self + other, Any) + +class Day(Base): + def apply(self, other: object): + return Day() + +assert_type(Day() + Day(), Any) + +def bounded[T: Base](value: T) -> None: + assert_type(value + object(), Any) + +class Direct: + def __add__(self, other: object): + return NotImplemented + +class DirectChild(Direct): + pass + +DirectChild() + object() # E: Cannot find `__add__` or `__radd__` + +class Declared(Base): + def __add__(self, other: object) -> NotImplementedType: + return self.apply(other) + +Declared() + object() # E: Cannot find `__add__` or `__radd__` + +class Overloaded: + def apply(self, other: object): + return NotImplemented + + @overload + def __add__(self, other: int) -> int: ... + @overload + def __add__(self, other: str) -> NotImplementedType: ... + def __add__(self, other: int | str): + return self.apply(other) + +class OverloadedChild(Overloaded): + pass + +assert_type(OverloadedChild() + 1, int) +OverloadedChild() + "x" # E: Cannot find `__add__` or `__radd__` + +class Value: + pass + +class Guarded: + def __add__(self, other: object): + if isinstance(other, Value): + return Value() + return NotImplemented + +class Reflected: + def __radd__(self, other: object) -> str: + return "" + +assert_type(Guarded() + Reflected(), Value | str) + "#, +); + +testcase!( + test_not_implemented_comparison_completion, + r#" +from types import NotImplementedType +from typing import Literal, Never, assert_type + +class Declines: + def __eq__(self, other: object) -> NotImplementedType: + return NotImplemented + + def __ne__(self, other: object) -> NotImplementedType: + return NotImplemented + + def __lt__(self, other: object) -> NotImplementedType: + return NotImplemented + + def __gt__(self, other: object) -> NotImplementedType: + return NotImplemented + +class Partial: + def __eq__(self, other: object) -> Literal[True] | NotImplementedType: + return NotImplemented + + def __ne__(self, other: object) -> Literal[False] | NotImplementedType: + return NotImplemented + +class Accepts: + def __eq__(self, other: object) -> Literal[True]: + return True + + def __ne__(self, other: object) -> Literal[False]: + return False + +def compare(left: Declines, right: Declines, partial: Partial, accepts: Accepts) -> None: + assert_type(left == right, bool) + assert_type(left != right, bool) + left < right # E: Cannot find `__lt__` or `__gt__` + assert_type(partial == partial, bool) + assert_type(partial != partial, bool) + assert_type(left == accepts, Literal[True]) + assert_type(left != accepts, Literal[False]) + assert_type(partial == accepts, Literal[True]) + assert_type(partial != accepts, Literal[False]) + +class Raises: + def __eq__(self, other: object) -> Never: + raise RuntimeError + +def no_identity_after_never(left: Raises, right: Raises) -> None: + assert_type(left == right, Never) + "#, +); + +testcase!( + test_not_implemented_inplace_stages, + r#" +from types import NotImplementedType +from typing import Never, assert_type + +class Right: + def __radd__(self, other: object) -> str: + return "" + +class Left: + def __iadd__(self, other: object) -> NotImplementedType: + return NotImplemented + + def __add__(self, other: object) -> int: + return 1 + +def ordinary_success() -> None: + left = Left() + left += Right() + assert_type(left, int) + +class PartialLeft(Left): + def __iadd__(self, other: object) -> bytes | NotImplementedType: + return NotImplemented + +def partial_inplace() -> None: + left = PartialLeft() + left += Right() + assert_type(left, bytes | int) + +class DecliningLeft(Left): + def __add__(self, other: object) -> NotImplementedType: + return NotImplemented + +def reflected_success() -> None: + left = DecliningLeft() + left += Right() + assert_type(left, str) + +class Raises: + def __iadd__(self, other: object) -> Never: + raise RuntimeError + +def nonreturning() -> None: + left = Raises() + left += Right() + assert_type(left, Never) + +class BadSignature(Left): + def __iadd__(self, other: Never) -> NotImplementedType: # E: Class member `BadSignature.__iadd__` overrides parent class `Left` in an inconsistent manner + return NotImplemented + +def signature_recovery() -> None: + left = BadSignature() + left += Right() + assert_type(left, int) + "#, +); + +testcase!( + test_not_implemented_deprecation, + r#" +from types import NotImplementedType +from typing import assert_type +from warnings import deprecated + +class Left: + @deprecated("Deprecated forward method") + def __add__(self, other: object) -> NotImplementedType: + return NotImplemented + +class Right: + @deprecated("Deprecated reflected method") + def __radd__(self, other: object) -> int: + return 1 + +assert_type(Left() + Right(), int) # E: `Left.__add__` is deprecated # E: `Right.__radd__` is deprecated + "#, +); + // We try __iadd__ and some fallback dunders. When all fail, the least confusing option is to use __iadd__. testcase!( test_iadd_error,