From 8dd33217fec5d45b0ccfb4ac6c80366f0423f305 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Thu, 25 Jun 2026 20:18:15 +0200 Subject: [PATCH 1/2] Add __trunc__ to ObjectProxy so math.trunc() works transparently 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. --- src/wrapt/wrappers.py | 4 ++++ tests/core/test_object_proxy.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/wrapt/wrappers.py b/src/wrapt/wrappers.py index eccc544a..6e98b5a1 100644 --- a/src/wrapt/wrappers.py +++ b/src/wrapt/wrappers.py @@ -1,6 +1,7 @@ """Core object proxy and function wrapper implementations.""" import inspect +import math import operator import sys import types @@ -261,6 +262,9 @@ def __reversed__(self): def __round__(self, ndigits=None): return round(self.__wrapped__, ndigits) + def __trunc__(self): + return math.trunc(self.__wrapped__) + def __mro_entries__(self, bases): if not isinstance(self.__wrapped__, type) and hasattr( self.__wrapped__, "__mro_entries__" diff --git a/tests/core/test_object_proxy.py b/tests/core/test_object_proxy.py index 0996dfff..71e1cb37 100644 --- a/tests/core/test_object_proxy.py +++ b/tests/core/test_object_proxy.py @@ -1973,6 +1973,34 @@ def test_fractions_round(self): self.assertEqual(round(instance, 3), round(proxy, 3)) self.assertEqual(round(instance, ndigits=3), round(proxy, ndigits=3)) + def test_float_trunc(self): + import math + + instance = 1.7 + proxy = wrapt.ObjectProxy(instance) + + self.assertEqual(math.trunc(instance), math.trunc(proxy)) + self.assertEqual(math.trunc(instance), 1) + + def test_negative_float_trunc(self): + import math + + instance = -1.7 + proxy = wrapt.ObjectProxy(instance) + + self.assertEqual(math.trunc(instance), math.trunc(proxy)) + self.assertEqual(math.trunc(instance), -1) + + def test_fractions_trunc(self): + import fractions + import math + + instance = fractions.Fraction("7/3") + proxy = wrapt.ObjectProxy(instance) + + self.assertEqual(math.trunc(instance), math.trunc(proxy)) + self.assertEqual(math.trunc(instance), 2) + class TestArgumentUnpacking(unittest.TestCase): From d2acfa5b9badb73de5b42e418d79138ea508b4a4 Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Fri, 26 Jun 2026 12:01:24 +0200 Subject: [PATCH 2/2] Proxy math integral special methods --- src/wrapt-stubs/__init__.pyi | 3 ++ src/wrapt/_wrappers.c | 61 +++++++++++++++++++++++++++++++++ src/wrapt/wrappers.py | 6 ++++ tests/core/test_object_proxy.py | 20 +++++++++++ 4 files changed, 90 insertions(+) diff --git a/src/wrapt-stubs/__init__.pyi b/src/wrapt-stubs/__init__.pyi index 0cca7a97..771d3004 100644 --- a/src/wrapt-stubs/__init__.pyi +++ b/src/wrapt-stubs/__init__.pyi @@ -149,6 +149,9 @@ if sys.version_info >= (3, 10): def __bytes__(self) -> bytes: ... def __index__(self) -> int: ... def __round__(self, ndigits: int | None = ..., /) -> Any: ... + def __trunc__(self) -> Any: ... + def __floor__(self) -> Any: ... + def __ceil__(self) -> Any: ... # Unary arithmetic. def __neg__(self) -> Any: ... diff --git a/src/wrapt/_wrappers.c b/src/wrapt/_wrappers.c index ec1fdd21..24516e7f 100644 --- a/src/wrapt/_wrappers.c +++ b/src/wrapt/_wrappers.c @@ -2537,6 +2537,64 @@ static PyObject *WraptObjectProxy_round(WraptObjectProxyObject *self, /* ------------------------------------------------------------------------- */ +static PyObject *WraptObjectProxy_call_math_unary(WraptObjectProxyObject *self, + const char *name) +{ + PyObject *module = NULL; + PyObject *function = NULL; + PyObject *result = NULL; + + if (!self->wrapped) + { + if (raise_uninitialized_wrapper_error(self) == -1) + return NULL; + } + + module = PyImport_ImportModule("math"); + + if (!module) + return NULL; + + function = PyObject_GetAttrString(module, name); + + if (!function) + { + Py_DECREF(module); + return NULL; + } + + Py_DECREF(module); + + result = PyObject_CallFunctionObjArgs(function, self->wrapped, NULL); + + Py_DECREF(function); + + return result; +} + +/* ------------------------------------------------------------------------- */ + +static PyObject *WraptObjectProxy_trunc(WraptObjectProxyObject *self) +{ + return WraptObjectProxy_call_math_unary(self, "trunc"); +} + +/* ------------------------------------------------------------------------- */ + +static PyObject *WraptObjectProxy_floor(WraptObjectProxyObject *self) +{ + return WraptObjectProxy_call_math_unary(self, "floor"); +} + +/* ------------------------------------------------------------------------- */ + +static PyObject *WraptObjectProxy_ceil(WraptObjectProxyObject *self) +{ + return WraptObjectProxy_call_math_unary(self, "ceil"); +} + +/* ------------------------------------------------------------------------- */ + static PyObject *WraptObjectProxy_complex(WraptObjectProxyObject *self, PyObject *args) { @@ -3148,6 +3206,9 @@ static PyMethodDef WraptObjectProxy_methods[] = { {"__reversed__", (PyCFunction)WraptObjectProxy_reversed, METH_NOARGS, 0}, {"__round__", (PyCFunction)WraptObjectProxy_round, METH_VARARGS | METH_KEYWORDS, 0}, + {"__trunc__", (PyCFunction)WraptObjectProxy_trunc, METH_NOARGS, 0}, + {"__floor__", (PyCFunction)WraptObjectProxy_floor, METH_NOARGS, 0}, + {"__ceil__", (PyCFunction)WraptObjectProxy_ceil, METH_NOARGS, 0}, {"__complex__", (PyCFunction)WraptObjectProxy_complex, METH_NOARGS, 0}, {"__mro_entries__", (PyCFunction)WraptObjectProxy_mro_entries, METH_VARARGS | METH_KEYWORDS, 0}, diff --git a/src/wrapt/wrappers.py b/src/wrapt/wrappers.py index 6e98b5a1..35d73c6a 100644 --- a/src/wrapt/wrappers.py +++ b/src/wrapt/wrappers.py @@ -265,6 +265,12 @@ def __round__(self, ndigits=None): def __trunc__(self): return math.trunc(self.__wrapped__) + def __floor__(self): + return math.floor(self.__wrapped__) + + def __ceil__(self): + return math.ceil(self.__wrapped__) + def __mro_entries__(self, bases): if not isinstance(self.__wrapped__, type) and hasattr( self.__wrapped__, "__mro_entries__" diff --git a/tests/core/test_object_proxy.py b/tests/core/test_object_proxy.py index 71e1cb37..acc1d4c9 100644 --- a/tests/core/test_object_proxy.py +++ b/tests/core/test_object_proxy.py @@ -1973,6 +1973,26 @@ def test_fractions_round(self): self.assertEqual(round(instance, 3), round(proxy, 3)) self.assertEqual(round(instance, ndigits=3), round(proxy, ndigits=3)) + def test_math_integral_protocol_methods(self): + import math + + class Instance: + def __trunc__(self): + return "truncated" + + def __floor__(self): + return "floored" + + def __ceil__(self): + return "ceiled" + + instance = Instance() + proxy = wrapt.ObjectProxy(instance) + + self.assertEqual(math.trunc(proxy), "truncated") + self.assertEqual(math.floor(proxy), "floored") + self.assertEqual(math.ceil(proxy), "ceiled") + def test_float_trunc(self): import math