Skip to content

Unwrap right hand proxy operand in pure Python binary operators - #357

Merged
GrahamDumpleton merged 1 commit into
developfrom
bugfix/proxy-operand-unwrapping
Sep 21, 2026
Merged

GrahamDumpleton merged 1 commit into
developfrom
bugfix/proxy-operand-unwrapping

Conversation

@GrahamDumpleton

Copy link
Copy Markdown
Owner

Problem

The C extension unwraps both operands of a binary operator before applying
it to the wrapped objects, but the pure Python implementation only unwrapped
self and passed a right hand proxy through to the wrapped object's
operator method:

def __add__(self, other):
    return self.__wrapped__ + other

This usually went unnoticed. The wrapped object's method returns
NotImplemented when handed a proxy, Python then tries the right hand
proxy's reflected method, and that unwraps the other side. The difference
is visible in two cases, both of which need a proxy on both sides:

  • The wrapped type raises TypeError for an operand it does not recognise,
    instead of returning NotImplemented. Python treats a raised exception as
    final and never tries the reflected method, so the right hand proxy is
    never unwrapped.
  • The right hand operand's type is a subclass of the left hand operand's
    type and overrides the reflected method. Python gives that method
    priority, but only when it sees the real types.
class Strict:
    def __add__(self, other):
        if type(other) is not Strict:
            raise TypeError("Strict only")
        return Strict(self.v + other.v)

ObjectProxy(Strict(1)) + ObjectProxy(Strict(2))
# C extension: Strict(3)
# pure Python: TypeError: Strict only
expression pure Python C extension
P(Strict(1)) + P(Strict(2)) TypeError Strict(3)
P(Base()) + P(Sub()) Base.__add__ Sub.__radd__

All 14 binary operators, their reflected forms and the 13 in-place forms
were affected.

Fix

The pure Python operator methods now unwrap a right hand operand which is
a proxy, via a _unwrap_operand() helper mirroring
wrapt_unwrap_operand() in the C extension. The result is the same as
applying the operator to the two wrapped objects in both implementations.
The real type is checked rather than __class__, which the proxy reports
as that of the wrapped object.

Where the left hand operand is not a proxy, Python has already chosen the
method to call before any wrapt code runs, so Base() + P(Sub()) still
cannot give Sub.__radd__ in either implementation. That is documented in
the change note rather than changed.

Tests

tests/core/test_proxy_operands.py adds 41 tests, one per affected
operator, in three groups: a wrapped type that raises TypeError, the
in-place forms of the same with and without an __i*__ method on the
wrapped type, and the subclass reflected priority case. Each derives its
expected value from the operation on the unwrapped objects. All 41 failed
with the pure Python implementation before the fix and passed with the C
extension. just test passes across the full matrix.

Docs

A change note is added under 2.4.2. The "Ternary pow()" known issue is
updated, as it stated the pure Python implementation accepts only a proxy
base. A proxy exponent alongside a proxy base now works in both
implementations. A proxy exponent with a plain base, and a proxy modulo,
still fail in both, and the entry now describes that shared behaviour.

The C extension unwraps both operands of a binary operator before
applying it to the wrapped objects, but the pure Python implementation
only unwrapped self and passed a right hand proxy through to the wrapped
object's operator method. This was hidden where the wrapped type returned
NotImplemented, as the right hand proxy's reflected method then unwrapped
the other side, but gave a different result to the C extension where the
wrapped type raised TypeError instead, or where the right hand operand
was a proxied subclass overriding the reflected method.

The pure Python binary, reflected and in-place operators now unwrap a
right hand operand which is a proxy, matching the C extension. Tests
cover all affected operators against the result of the operation on the
unwrapped objects. The ternary pow() known issue is updated as a proxy
exponent now works alongside a proxy base in both implementations.
@GrahamDumpleton
GrahamDumpleton merged commit 6a5e262 into develop Sep 21, 2026
39 checks passed
@GrahamDumpleton
GrahamDumpleton deleted the bugfix/proxy-operand-unwrapping branch September 21, 2026 23:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant