mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
fix(vm): keep str's concat error when __radd__ declines (#8652)
`PyStr::__add__` delegated to the right operand's `__radd__` whenever it had
one, and returned whatever came back. For every numeric type that is a
`NotImplemented`, which then reached the generic binary-op handler:
>>> "a" + 1
TypeError: unsupported operand type(s) for +: 'str' and 'int'
CPython falls back to `str`'s `sq_concat` once the reflected call declines, so
it names the concatenation instead:
TypeError: can only concatenate str (not "int") to str
The specific message was already here, but only reachable for operands with no
`__radd__` at all, so `list`, `object`, `bytes` and `None` were correct while
`int`, `float`, `bool` and any class with a declining `__radd__` were not.
A `__radd__` that returns a value still wins, unchanged.
Verified against CPython 3.14.0: the new snippet asserts the message for
`int`, `float`, `bool`, `list`, `None`, `bytes` and a declining `__radd__`,
and passes under both interpreters. Reverting only the `str.rs` change fails
its first assertion.
Assisted-by: Claude Code:claude-opus-5
This commit is contained in:
1 parent
25b5dfe5ca
commit
4f55bde7c8
2 files changed
+46
-3
No files matched your search
@@ -653,10 +653,17 @@ impl PyStr {
|
||||
Self::new_str_unchecked(bytes.into(), kind)
|
||||
}
|
||||
.to_pyobject(vm))
|
||||
} else if let Some(radd) = vm.get_method(other.clone(), identifier!(vm, __radd__)) {
|
||||
// hack to get around not distinguishing number add from seq concat
|
||||
radd?.call((zelf,), vm)
|
||||
} else {
|
||||
// hack to get around not distinguishing number add from seq concat
|
||||
if let Some(radd) = vm.get_method(other.clone(), identifier!(vm, __radd__)) {
|
||||
let result = radd?.call((zelf,), vm)?;
|
||||
// CPython reaches `str`'s sq_concat once the reflected call declines, so a
|
||||
// `__radd__` returning NotImplemented must still report the concat error
|
||||
// rather than the generic binary-op one.
|
||||
if !result.is(&vm.ctx.not_implemented) {
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
Err(vm.new_type_error(format!(
|
||||
r#"can only concatenate str (not "{}") to str"#,
|
||||
other.class().slot_name()
|
||||
|
||||
@@ -979,3 +979,39 @@ def test_expandtabs_zero_tabsize():
|
||||
|
||||
|
||||
test_expandtabs_zero_tabsize()
|
||||
|
||||
|
||||
def test_concat_error_message():
|
||||
# CPython falls back to str's sequence concatenation once a reflected
|
||||
# __radd__ declines, so the message names the concatenation, not the
|
||||
# generic binary operation. Types that define __radd__ (every number)
|
||||
# used to take the generic path here.
|
||||
def message(other):
|
||||
try:
|
||||
"a" + other
|
||||
except TypeError as err:
|
||||
return str(err)
|
||||
raise AssertionError("TypeError was not raised")
|
||||
|
||||
assert message(1) == 'can only concatenate str (not "int") to str'
|
||||
assert message(1.5) == 'can only concatenate str (not "float") to str'
|
||||
assert message(True) == 'can only concatenate str (not "bool") to str'
|
||||
assert message([]) == 'can only concatenate str (not "list") to str'
|
||||
assert message(None) == 'can only concatenate str (not "NoneType") to str'
|
||||
assert message(b"b") == 'can only concatenate str (not "bytes") to str'
|
||||
|
||||
class Declines:
|
||||
def __radd__(self, other):
|
||||
return NotImplemented
|
||||
|
||||
assert message(Declines()) == 'can only concatenate str (not "Declines") to str'
|
||||
|
||||
# A __radd__ that returns a value still wins.
|
||||
class Accepts:
|
||||
def __radd__(self, other):
|
||||
return "from-radd"
|
||||
|
||||
assert "a" + Accepts() == "from-radd"
|
||||
|
||||
|
||||
test_concat_error_message()
|
||||
Reference in new issue
Block a user