Skip to content
Merged
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
39 changes: 31 additions & 8 deletions crates/vm/src/types/slot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,11 @@ macro_rules! number_binary_op_wrapper {
|a, b, vm| vm.call_special_method(a, identifier!(vm, $name), (b.to_owned(),))
};
}
// binary_op1 needs one function address across slot updates and deletions.
fn number_add_wrapper(a: &PyObject, b: &PyObject, vm: &VirtualMachine) -> PyResult {
vm.call_special_method(a, identifier!(vm, __add__), (b.to_owned(),))
}

macro_rules! number_binary_right_op_wrapper {
($name:ident) => {
|a, b, vm| vm.call_special_method(b, identifier!(vm, $name), (a.to_owned(),))
Expand Down Expand Up @@ -888,7 +893,7 @@ impl PyType {
name: &'static PyStrInterned,
ctx: &Context,
) {
use crate::builtins::descriptor::SlotFunc;
use crate::builtins::descriptor::{PyWrapper, SlotFunc};

// Helper macro for main slots
macro_rules! update_main_slot {
Expand Down Expand Up @@ -1250,13 +1255,31 @@ impl PyType {
number_binary_right_op_wrapper!(__radd__),
NumBinary
)
} else {
update_sub_slot!(
as_number,
add,
number_binary_op_wrapper!(__add__),
NumBinary
)
}
let native_concat = self
.get_attr(identifier!(ctx, __add__))
.is_some_and(|attr| {
attr.class().is(ctx.types.wrapper_descriptor_type)
&& attr.downcast_ref::<PyWrapper>().is_some_and(|descr| {
descr.name.as_str() == "__add__"
&& self.mro.read().iter().any(|cls| cls.is(descr.typ))
&& matches!(descr.wrapped, SlotFunc::SeqConcat(_))
})
});
if native_concat {
// Native sequence addition stays in sq_concat unless nb_add
// already participates or __radd__ needs its shared dispatcher.
// Like CPython's resolve_slotdups, preserve an existing numeric
// wrapper when an override is replaced or deleted.
let needs_numeric = self.slots.as_number.add.load().is_some()
|| self.slots.as_sequence.concat.load().is_none()
|| self.get_attr(identifier!(ctx, __radd__)).is_some();
self.slots
.as_number
.add
.store(needs_numeric.then_some(number_add_wrapper));
} else if name.as_str() == "__add__" {
update_sub_slot!(as_number, add, number_add_wrapper, NumBinary)
}
}
SlotAccessor::NbInplaceAdd => {
Expand Down
91 changes: 91 additions & 0 deletions extra_tests/snippets/builtin_str_add.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,94 @@ class Child(Override):
del Override.__add__
for cls in (Override, Child):
assert operator.add(cls(left), right) == left + right


# Inheriting native concatenation must leave reflected numeric addition first.
for base, left in (
(str, "a"),
(bytes, b"a"),
(tuple, (1,)),
(list, [1]),
(bytearray, b"a"),
):

class Native(base):
pass

class Child(Native):
pass

class Explicit(base):
__add__ = base.__add__

for cls in (Native, Child, Explicit):
for result in ("reflected", NotImplemented):
other = Reflected(result)
if result is NotImplemented:
assert_raises(TypeError, operator.add, cls(left), other)
else:
assert operator.add(cls(left), other) == result
assert other.calls == 1

class ReflectedSequence(base):
calls = 0

def __radd__(self, other):
self.calls += 1
return NotImplemented

other = ReflectedSequence(left)
assert operator.add(Native(left), other) == base(left) + base(left)
assert other.calls == 1

class AddOverride(ReflectedSequence):
def __add__(self, other):
return "overridden"

other = AddOverride(left)
assert operator.add(ReflectedSequence(left), other) == base(left) + base(left)
assert other.calls == 0
del AddOverride.__add__
other = AddOverride(left)
assert operator.add(ReflectedSequence(left), other) == base(left) + base(left)
assert other.calls == 0

# CPython keeps numeric dispatch after either addition method is overridden,
# including when deletion or replacement exposes the native __add__ again.
Native.__radd__ = lambda self, other: NotImplemented
for change in (
lambda: None,
lambda: delattr(Native, "__radd__"),
lambda: setattr(Native, "__add__", base.__add__),
lambda: delattr(Native, "__add__"),
):
change()
for cls in (Native, Child):
other = Reflected("reflected")
assert_raises(TypeError, operator.add, cls(left), other)
assert other.calls == 0


# A differently named descriptor assigned to __add__ needs numeric dispatch.
class InplaceAlias(list):
__add__ = list.__iadd__


other = Reflected("reflected")
assert_raises(TypeError, operator.add, InplaceAlias([1]), other)
assert other.calls == 0


class NativeIntAdd(int):
__add__ = int.__add__

def __radd__(self, other):
raise AssertionError("inherited reflected addition took priority")


class IntAddOverride(NativeIntAdd):
def __add__(self, other):
return "overridden"


assert operator.add(NativeIntAdd(1), IntAddOverride(2)) == 3
Loading