fix special-case NotImplementedType in binop signatures to match runtime semantics #1129#2677
fix special-case NotImplementedType in binop signatures to match runtime semantics #1129#2677asukaminato0721 wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes issue #1129, where binary operations involving dunder methods that return NotImplementedType would incorrectly leak the NotImplementedType into the inferred result type instead of following Python's runtime semantics (trying the reflected dunder when the forward one signals NotImplemented).
Changes:
- The
try_binop_callslogic inoperators.rsis updated to stripNotImplementedTypefrom successful dunder return types, accumulate non-NotImplementedTyperesults, and continue searching for reflected dunders when needed. NotImplementedTypeis added as a new stdlib entry inStdlib, gated to Python ≥ 3.10 (consistent with howEllipsisTypeis handled).- A regression test is added covering both the pure-
NotImplementedTypefallback and the mixedint | NotImplementedTypescenario.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
pyrefly/lib/alt/operators.rs |
Core fix: strips NotImplementedType from successful dunder results, accumulates partial results, and continues to reflected dunders when needed |
crates/pyrefly_types/src/stdlib.rs |
Adds NotImplementedType as an Option<StdlibResult<ClassType>> stdlib entry, guarded by version >= 3.10 |
pyrefly/lib/test/operators.rs |
Regression test covering the bug from issue #1129 for both pure and mixed NotImplementedType return types |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| errors.extend(callee_errors); | ||
| return ret; | ||
| if ret_without_not_implemented != ret { | ||
| successful_ret = self.union(successful_ret, ret_without_not_implemented); | ||
| continue; |
There was a problem hiding this comment.
When a dunder returns int | NotImplementedType, the code extends errors with callee_errors at line 204 and then continues to look for a reflected dunder. If callee_errors is non-empty (e.g., because the method is not callable as a call target), those errors are emitted unconditionally — even if the reflected dunder later succeeds cleanly. This results in spurious error reporting.
The callee_errors extension should be deferred: accumulate them alongside successful_ret and only emit them at the return site (line 209 or line 215), similar to how first_call defers error emission until it is determined whether the call is ultimately the "best" result.
| }), | ||
| None => ret.clone(), | ||
| }; | ||
| if ret_without_not_implemented.is_never() { |
There was a problem hiding this comment.
When all dunder methods exist and have no call errors, but all return only NotImplementedType (so ret_without_not_implemented.is_never() is true for every iteration), the code falls through to the "Cannot find __add__ or __radd__" error message. This message is misleading: the methods do exist, they just always return NotImplemented. A more accurate message could indicate that all matching operator methods always return NotImplemented.
Note: this scenario only arises in uncommon code where every dunder is annotated to always return NotImplementedType.
| if ret_without_not_implemented.is_never() { | |
| if ret_without_not_implemented.is_never() { | |
| // All branches of this dunder call resolved to NotImplementedType. | |
| // Record this call as the first attempted call (if none recorded yet) | |
| // so that later error handling can distinguish "methods exist but | |
| // always return NotImplemented" from "no dunder methods found". | |
| if first_call.is_none() { | |
| first_call = Some((callee_errors, call_errors, ret)); | |
| } |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Diff from mypy_primer, showing the effect of this PR on open source code: freqtrade (https://github.com/freqtrade/freqtrade)
+ ERROR freqtrade/data/converter/orderflow.py:175:21-76: `-` is not supported between `date` and `Timestamp` [unsupported-operation]
+ ERROR freqtrade/data/converter/orderflow.py:175:21-76: `-` is not supported between `date` and `datetime` [unsupported-operation]
pandas-stubs (https://github.com/pandas-dev/pandas-stubs)
+ ERROR tests/indexes/bool/test_mul.py:93:20-37: assert_type(Any, Never) failed [assert-type]
+ ERROR tests/indexes/bool/test_mul.py:94:20-37: assert_type(ndarray[tuple[Any, ...], dtype[timedelta64]], Never) failed [assert-type]
+ ERROR tests/indexes/bool/test_sub.py:61:20-37: assert_type(Unknown, Never) failed [assert-type]
+ ERROR tests/indexes/bool/test_sub.py:61:21-29: `-` is not supported between `Index[builtins.bool]` and `ndarray[tuple[Any, ...], dtype[numpy.bool]]` [unsupported-operation]
+ ERROR tests/indexes/bool/test_sub.py:70:20-37: assert_type(Unknown, Never) failed [assert-type]
+ ERROR tests/indexes/bool/test_sub.py:70:21-29: `-` is not supported between `ndarray[tuple[Any, ...], dtype[numpy.bool]]` and `Index[builtins.bool]` [unsupported-operation]
+ ERROR tests/indexes/bool/test_truediv.py:70:20-37: assert_type(ndarray[tuple[Any, ...], dtype[float64]], Never) failed [assert-type]
+ ERROR tests/indexes/complex/test_sub.py:63:26-46: assert_type(Index[complex], NoReturn) failed [assert-type]
+ ERROR tests/indexes/float/test_sub.py:63:26-46: assert_type(Index[float], NoReturn) failed [assert-type]
+ ERROR tests/indexes/float/test_truediv.py:88:20-37: assert_type(Any, Never) failed [assert-type]
+ ERROR tests/indexes/float/test_truediv.py:89:20-37: assert_type(ndarray[tuple[Any, ...], dtype[float64]], Never) failed [assert-type]
+ ERROR tests/indexes/int/test_floordiv.py:90:20-38: assert_type(Any, Never) failed [assert-type]
+ ERROR tests/indexes/int/test_floordiv.py:91:20-38: assert_type(Any, Never) failed [assert-type]
+ ERROR tests/indexes/int/test_floordiv.py:92:20-38: assert_type(ndarray[tuple[Any, ...], dtype[signedinteger[_64Bit]]], Never) failed [assert-type]
+ ERROR tests/indexes/int/test_sub.py:63:26-46: assert_type(Index[int], NoReturn) failed [assert-type]
+ ERROR tests/indexes/int/test_truediv.py:88:20-37: assert_type(Any, Never) failed [assert-type]
+ ERROR tests/indexes/int/test_truediv.py:89:20-37: assert_type(ndarray[tuple[Any, ...], dtype[float64]], Never) failed [assert-type]
+ ERROR tests/indexes/timedeltaindex/test_floordiv.py:86:20-38: assert_type(ndarray[tuple[Any, ...], dtype[signedinteger[_8Bit]]], Never) failed [assert-type]
+ ERROR tests/indexes/timedeltaindex/test_floordiv.py:90:20-38: assert_type(Any, Never) failed [assert-type]
+ ERROR tests/indexes/timedeltaindex/test_floordiv.py:91:20-38: assert_type(Any, Never) failed [assert-type]
+ ERROR tests/indexes/timedeltaindex/test_mul.py:74:20-37: assert_type(ndarray[tuple[Any, ...], dtype[numpy.bool]], Never) failed [assert-type]
+ ERROR tests/indexes/timedeltaindex/test_mul.py:78:20-37: assert_type(ndarray[tuple[Any, ...], dtype[complex128]], Never) failed [assert-type]
+ ERROR tests/indexes/timedeltaindex/test_truediv.py:85:20-37: assert_type(ndarray[tuple[Any, ...], dtype[float64]], Never) failed [assert-type]
+ ERROR tests/indexes/timedeltaindex/test_truediv.py:89:20-37: assert_type(ndarray[tuple[Any, ...], dtype[complex128]], Never) failed [assert-type]
+ ERROR tests/indexes/timedeltaindex/test_truediv.py:90:20-37: assert_type(Any, Never) failed [assert-type]
+ ERROR tests/series/bool/test_sub.py:89:20-37: assert_type(Unknown, Never) failed [assert-type]
+ ERROR tests/series/bool/test_sub.py:89:21-29: `-` is not supported between `Series[builtins.bool]` and `ndarray[tuple[Any, ...], dtype[numpy.bool]]` [unsupported-operation]
+ ERROR tests/series/bool/test_sub.py:98:20-37: assert_type(Unknown, Never) failed [assert-type]
+ ERROR tests/series/bool/test_sub.py:98:21-29: `-` is not supported between `ndarray[tuple[Any, ...], dtype[numpy.bool]]` and `Series[builtins.bool]` [unsupported-operation]
+ ERROR tests/series/complex/test_sub.py:98:26-46: assert_type(Series[complex], NoReturn) failed [assert-type]
+ ERROR tests/series/float/test_sub.py:86:26-46: assert_type(Series[float], NoReturn) failed [assert-type]
+ ERROR tests/series/int/test_sub.py:86:26-46: assert_type(Series[int], NoReturn) failed [assert-type]
+ ERROR tests/series/timedelta/test_sub.py:112:20-37: assert_type(ndarray[tuple[Any, ...], dtype[timedelta64]], Never) failed [assert-type]
pandas (https://github.com/pandas-dev/pandas)
+ ERROR pandas/core/indexes/interval.py:1509:32-43: `-` is not supported between `date` and `Timestamp` [unsupported-operation]
+ ERROR pandas/core/indexes/interval.py:1509:32-43: `-` is not supported between `date` and `datetime` [unsupported-operation]
werkzeug (https://github.com/pallets/werkzeug)
+ ERROR tests/test_datastructures.py:710:13-33: `|` is not supported between `EnvironHeaders` and `dict[str, str]` [unsupported-operation]
+ ERROR tests/test_datastructures.py:716:13-34: `|=` is not supported between `EnvironHeaders` and `dict[str, str]` [unsupported-operation]
core (https://github.com/home-assistant/core)
+ ERROR homeassistant/components/local_calendar/calendar.py:224:13-24: `-` is not supported between `date` and `datetime` [unsupported-operation]
|
Primer Diff Classification❌ 5 regression(s) | 5 project(s) total | +38 errors 5 regression(s) across freqtrade, pandas-stubs, pandas, werkzeug, core. error kinds:
Detailed analysis❌ Regression (5)freqtrade (+1)
pandas-stubs (+33)
23/33 errors contain
pandas (+1)
werkzeug (+2)
The PR's new logic in The Error 1 (line 710) is pyrefly-only, confirming it's likely a false positive. Error 2 (line 716) is co-reported by pyright, but pyright may have its own reasons for flagging Per-category reasoning:
Looking more carefully: For core (+1)
The error message states The PR's changes to binary operator resolution in
Suggested fixesSummary: The PR's NotImplementedType stripping logic in try_dunder_call_pairs() incorrectly treats NoReturn/Never return types from dunder methods the same as NotImplementedType, causing false positive unsupported-operation errors across 5 projects. **1. In if ret_without_not_implemented.[`is_never()`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/operators.rs) {
if ret_without_not_implemented != ret {
// NotImplementedType was stripped and nothing remained — skip to next dunder
continue;
} else {
// Original return was already Never/NoReturn — this is a valid resolution (method raises)
errors.extend(callee_errors);
return self.union(successful_ret, ret);
}
}This preserves the intended behavior of stripping NotImplementedType while correctly handling NoReturn dunders.**
**2. In The fix: when a dunder returns a partial NotImplementedType union, after stripping NotImplementedType and accumulating the non-NotImplemented part, the code should still try reflected dunders for the NotImplementedType portion. But critically, if the reflected dunder succeeds, its result should also be accumulated into successful_ret. Currently the code does A more targeted fix: before the if ret_without_not_implemented.[`is_never()`](https://github.com/facebook/pyrefly/blob/main/pyrefly/lib/alt/operators.rs) && ret_without_not_implemented != ret {
// Pure NotImplementedType return — skip to reflected dunder
continue;
}
if ret_without_not_implemented != ret {
// Partial NotImplementedType — accumulate successful part and continue
// to try reflected dunder for the NotImplemented cases
successful_ret = self.union(successful_ret, ret_without_not_implemented);
continue;
}
// No NotImplementedType at all — return immediately
errors.extend(callee_errors);
return self.union(successful_ret, ret);This is essentially what the current code does, but combined with the NoReturn fix above, it should handle the
**3. In For the 29 assert-type failures: tests like The NoReturn fix (suggestion 1) is the highest priority and most clearly correct. The pandas-stubs issues may partially resolve once the NoReturn/Never distinction is fixed, since some of the 29 failures may involve dunder methods that return NoReturn. For the remaining cases, the accumulation of partial results via No additional code change suggested beyond suggestions 1 and 2 — the pandas-stubs issues likely share the same root causes.**
Was this helpful? React with 👍 or 👎 Classification by primer-classifier (5 LLM) |
Summary
Fixes #1129
fixed the binop resolution path so NotImplementedType no longer leaks as a possible operator result.
successful dunder calls now strip only the exact NotImplementedType branch, keep searching reflected dunders when needed, and union any concrete results that can actually occur at runtime.
Test Plan
a regression test covering both pure fallback and mixed `int | NotImplementedType behavior.