Skip to content

Add __trunc__ to ObjectProxy so math.trunc() works transparently#344

Open
gaoflow wants to merge 2 commits into
GrahamDumpleton:developfrom
gaoflow:fix/object-proxy-trunc
Open

Add __trunc__ to ObjectProxy so math.trunc() works transparently#344
gaoflow wants to merge 2 commits into
GrahamDumpleton:developfrom
gaoflow:fix/object-proxy-trunc

Conversation

@gaoflow

@gaoflow gaoflow commented Jun 25, 2026

Copy link
Copy Markdown

Summary

ObjectProxy already implements __round__ (so round() works on
proxied numeric objects), but __trunc__ was absent. In Python 3.11+
math.trunc() looks up __trunc__ via the type, not the instance,
so ObjectProxy.__getattr__ forwarding does not help:

import wrapt, math

p = wrapt.ObjectProxy(1.7)

round(p)       # 2  ✓  (ObjectProxy.__round__ exists)
math.trunc(p)  # TypeError: type ObjectProxy doesn't define __trunc__ method

This is inconsistent:

  • round(proxy) works via ObjectProxy.__round__
  • math.ceil(proxy) / math.floor(proxy) happen to work via CPython's
    __float__ fallback in those builtins
  • math.trunc(proxy) fails with a TypeError

Fix

Add __trunc__ to _ObjectProxyMethods, mirroring the __round__ /
round() pattern:

def __trunc__(self):
    return math.trunc(self.__wrapped__)

Three regression tests are included covering float, negative float,
and fractions.Fraction proxied values.

Relation to issue #296

Issue #296 listed __trunc__ as a missing method and was closed
without implementing it. This PR provides a minimal, focused fix with
test coverage.


This pull request was prepared with the assistance of AI, under my direction and review.

ObjectProxy already implements __round__ (so round() works), but was
missing __trunc__, causing math.trunc() to raise TypeError for any
wrapped numeric type:

    >>> import wrapt, math
    >>> math.trunc(wrapt.ObjectProxy(1.7))
    TypeError: type ObjectProxy doesn't define __trunc__ method

In Python 3.11+ math.trunc() looks up __trunc__ via the type (not
instance), so ObjectProxy.__getattr__ forwarding does not help.  Add an
explicit __trunc__ that delegates to math.trunc(self.__wrapped__),
mirroring the pattern already used by __round__ / round().

Also add three regression tests covering float, negative float, and
fractions.Fraction wrapped values.
@GrahamDumpleton

Copy link
Copy Markdown
Owner

The __trunc__ dunder method was not implemented because as was mentioned in the linked issue it sat outside of the core Python object model. Since the math module encapsulates pretty basic maths operations though I might consider adding it, but would probably need to also add __floor__ and __ceil__ if there are math module functions for those as well.

Note though that the change proposed doesn't include C API implementations which if the tests work without, suggests that math.trunc might be working even without the special dunder method. This might be an issue with the tests though since a more proper test might be to wrap an object which itself defines __trunc__ in a way that behaves in special way.

@gaoflow

gaoflow commented Jun 26, 2026

Copy link
Copy Markdown
Author

Thanks, that makes sense. I updated the PR to cover the full math integral protocol surface:

  • added __trunc__, __floor__, and __ceil__ on the pure Python ObjectProxy
  • added the same methods to the C extension method table, delegating through math.trunc/floor/ceil on the wrapped object
  • updated the stubs
  • added a stronger regression test using a wrapped custom object whose __trunc__, __floor__, and __ceil__ return sentinel values, so the test verifies special method forwarding rather than relying on float behavior

Verification:

  • WRAPT_DISABLE_EXTENSIONS=true uv run --with pytest --with-editable . python -m pytest tests/core/test_object_proxy.py -q -> 190 passed
  • WRAPT_INSTALL_EXTENSIONS=true uv run --with pytest --with-editable . python -m pytest tests/core/test_object_proxy.py -q -> 190 passed
  • WRAPT_DISABLE_EXTENSIONS=true uv run --with pytest --with-editable . python -m pytest tests -q -> 1042 passed, 8 skipped
  • WRAPT_INSTALL_EXTENSIONS=true uv run --with pytest --with-editable . python -m pytest tests -q -> 1044 passed, 6 skipped
  • git diff --check -> passed

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.

2 participants