Skip to content

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

Merged
GrahamDumpleton merged 2 commits into
GrahamDumpleton:developfrom
gaoflow:fix/object-proxy-trunc
Jul 10, 2026
Merged

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

Conversation

@gaoflow

@gaoflow gaoflow commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

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
Contributor 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

@GrahamDumpleton

Copy link
Copy Markdown
Owner

I'll merge this, but going to make a few changes afterwards and add more tests.

@GrahamDumpleton
GrahamDumpleton merged commit 3c1e45f into GrahamDumpleton:develop Jul 10, 2026
49 checks passed
GrahamDumpleton added a commit that referenced this pull request Jul 10, 2026
Rename C helper function to follow naming convention for internal
helpers, add conventional unused second argument to METH_NOARGS
functions, extend test coverage for __floor__ and __ceil__ including
precedence over __float__ fallback, and add change notes.
jkennedyvz pushed a commit to langchain-ai/langsmith-sdk that referenced this pull request Aug 4, 2026
…3323)

Bumps the py-major group in /python with 2 updates:
[wrapt](https://github.com/GrahamDumpleton/wrapt) and
[multipart](https://github.com/defnull/multipart).

Updates `wrapt` from 1.17.3 to 2.3.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/GrahamDumpleton/wrapt/releases">wrapt's
releases</a>.</em></p>
<blockquote>
<h2>wrapt 2.3.0</h2>
<p>Full release notes: <a
href="https://wrapt.readthedocs.io/en/latest/changes.html#version-2-3-0">https://wrapt.readthedocs.io/en/latest/changes.html#version-2-3-0</a></p>
<p>Install from PyPi (recommended):</p>
<pre><code>pip install wrapt==2.3.0
</code></pre>
<p>PyPi uploads follow each GitHub release; if <code>pip</code> reports
the
version is unavailable, the matching PyPi upload may not have
happened yet.</p>
<p>Pre-built wheels are provided for a range of Python versions
and platforms (Linux x86_64/aarch64/riscv64, macOS x86_64 and
arm64, Windows x86_64 and arm64, plus PyPy and free-threaded
builds). The source distribution is also attached together
with <code>SHA256SUMS</code> for verification.</p>
<h2>wrapt 2.3.0rc2</h2>
<p>Release candidate. Release notes for the upcoming
2.3.0 final (work in progress): <a
href="https://wrapt.readthedocs.io/en/latest/changes.html#version-2-3-0">https://wrapt.readthedocs.io/en/latest/changes.html#version-2-3-0</a></p>
<p>May be installable from PyPi:</p>
<pre><code>pip install wrapt==2.3.0rc2
</code></pre>
<p>If <code>pip</code> reports the version is unavailable, this
candidate
either has not been uploaded yet or is not being published to
PyPi. Use the attached wheels or build from the source
distribution instead:</p>
<pre><code>tar xf wrapt-2.3.0rc2.tar.gz
cd wrapt-2.3.0rc2
pip install .
</code></pre>
<p><code>SHA256SUMS</code> is attached for verification of the
archives.</p>
<h2>wrapt 2.3.0rc1</h2>
<p>Release candidate. Release notes for the upcoming
2.3.0 final (work in progress): <a
href="https://wrapt.readthedocs.io/en/latest/changes.html#version-2-3-0">https://wrapt.readthedocs.io/en/latest/changes.html#version-2-3-0</a></p>
<p>May be installable from PyPi:</p>
<pre><code>pip install wrapt==2.3.0rc1
</code></pre>
<p>If <code>pip</code> reports the version is unavailable, this
candidate
either has not been uploaded yet or is not being published to
PyPi. Use the attached wheels or build from the source
distribution instead:</p>
<pre><code>tar xf wrapt-2.3.0rc1.tar.gz
</code></pre>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/GrahamDumpleton/wrapt/blob/develop/docs/changes.rst">wrapt's
changelog</a>.</em></p>
<blockquote>
<h2>Version 2.3.0</h2>
<p><strong>New Features</strong></p>
<ul>
<li>
<p>The <code>__trunc__()</code>, <code>__floor__()</code> and
<code>__ceil__()</code> special methods
are now implemented by object proxies, delegating to
<code>math.trunc()</code>,
<code>math.floor()</code> and <code>math.ceil()</code> applied to the
wrapped object. As
with other special methods, these are only looked up on the class type
and not the instance, so they cannot rely on the
<code>__getattr__()</code>
fallback of the proxy and must be implemented explicitly. Previously
calling <code>math.trunc()</code> on an object proxy raised
<code>TypeError</code>. These
special methods sit somewhat outside the core Python object model in
that
they are not used by any builtin operators, with the <code>math</code>
module
being their only consumer. They are however documented as part of the
Python data model and the <code>math</code> module is a key module in
the standard
library, so supporting them is warranted, in the same way as the
existing
support for <code>__round__()</code>, which is consumed by the
<code>round()</code>
builtin. Note that although <code>math.floor()</code> and
<code>math.ceil()</code>
previously appeared to work when used on an object proxy, they were
silently falling back to converting the proxy using
<code>__float__()</code>. If
the wrapped object provided its own <code>__floor__()</code> or
<code>__ceil__()</code>
special methods these were ignored and the result could differ from that
when the wrapped object was used directly. These now yield the same
result as using the wrapped object directly. With thanks to Vincent Gao
for <code>pull request
[#344](GrahamDumpleton/wrapt#344)
&lt;https://github.com/GrahamDumpleton/wrapt/pull/344&gt;</code>_.</p>
</li>
<li>
<p>The <code>__fspath__()</code> special method of the
<code>os.PathLike</code> protocol has
been added to the set of dunder methods which
<code>AutoObjectProxy</code>
detects on the wrapped object and adds to the class it generates, so a
proxy it creates around a path-like object can now be used with
<code>os.fspath()</code>, the builtin <code>open()</code> and other
standard library
functions accepting paths. Note that <code>__fspath__()</code> is
deliberately
not implemented by the base object proxy, since its presence on the
proxy type would cause every proxy to be classified as path-like by
code branching on <code>isinstance(obj, os.PathLike)</code>. Also be
aware that
<code>AutoObjectProxy</code> creates a new class for every proxy
instance, so it
should not be used to wrap path-like objects in large numbers due to
the memory overhead. For high-frequency use define a custom proxy class
which adds an explicit <code>__fspath__()</code> method instead. See the
section
on wrapping path-like objects in the known issues documentation for
more details.</p>
</li>
</ul>
<p><strong>Features Changed</strong></p>
<ul>
<li>The type stubs have been aligned with the runtime behaviour of the
code
and are now verified by <code>stubtest</code> against both the C
extension and
pure Python implementations. If using a type checker there are a couple
of changes in what will be accepted which may be noticed. The</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/GrahamDumpleton/wrapt/commit/95d5d01bd671186000c02e1d0f6806a4d7c5d217"><code>95d5d01</code></a>
Merge branch 'release/2.3.0'</li>
<li><a
href="https://github.com/GrahamDumpleton/wrapt/commit/de094674f4d5e59b1b69c3b042ecc9b0eac729e7"><code>de09467</code></a>
Update to 2.3.0 for final release.</li>
<li><a
href="https://github.com/GrahamDumpleton/wrapt/commit/f2f807e002e6fab134cb7599b34b93cadc900bf1"><code>f2f807e</code></a>
Update to 2.3.0rc2.</li>
<li><a
href="https://github.com/GrahamDumpleton/wrapt/commit/4f6223ef946941e68352752dee1815bb2cb6ec9c"><code>4f6223e</code></a>
Acknowledge reporter in bytes() bug fix change log entry.</li>
<li><a
href="https://github.com/GrahamDumpleton/wrapt/commit/783d05877279e51d288466c0c0b37487c2034cf2"><code>783d058</code></a>
Extend tests for bytes() conversion of object proxies.</li>
<li><a
href="https://github.com/GrahamDumpleton/wrapt/commit/300d23d406ff929d169663d8f98347271add017d"><code>300d23d</code></a>
Merge pull request <a
href="https://redirect.github.com/GrahamDumpleton/wrapt/issues/345">#345</a>
from Sanjays2402/fix/bytes-proxy-int</li>
<li><a
href="https://github.com/GrahamDumpleton/wrapt/commit/6cf696233e4d777cda88c6ee06053bf71f73a445"><code>6cf6962</code></a>
Make bytes() on a proxy match bytes() on the wrapped object</li>
<li><a
href="https://github.com/GrahamDumpleton/wrapt/commit/58557a7cf87e325db675c9dfdf218600bec77daa"><code>58557a7</code></a>
Update to 2.3.0rc1.</li>
<li><a
href="https://github.com/GrahamDumpleton/wrapt/commit/2fe996189f298f7951aef75e1b63a9a585f1cff3"><code>2fe9961</code></a>
Document buffer protocol behaviour of object proxies.</li>
<li><a
href="https://github.com/GrahamDumpleton/wrapt/commit/818191bef9075a07eea1092446123d2fdc5c6af6"><code>818191b</code></a>
Force and verify C extension build in test-stubtest recipe.</li>
<li>Additional commits viewable in <a
href="https://github.com/GrahamDumpleton/wrapt/compare/1.17.3...2.3.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `multipart` from 1.3.1 to 2.0.0
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/defnull/multipart/blob/main/CHANGELOG.rst">multipart's
changelog</a>.</em></p>
<blockquote>
<h1>=========
Changelog</h1>
<p>This project follows Semantic Versioning
(<code>major.minor.patch</code>), with the
exception that behavior changes are allowed in minor releases as long as
the
change corrects behavior to match documentation, specification or
expectation. In other words: Bugfixes do not count as backward
incompatible
changes, even if they technically change behavior from
<em>incorrect</em> to <em>correct</em>
and may break applications that rely on <em>incorrect</em> or
<em>undefined</em> behavior or
<em>undocumented</em> APIs. EOL Python versions may also be dropped
during <em>minor</em>
releases.</p>
<h1>Release 2.0</h1>
<p>This release drops EOL python versions, improves error handling and
includes multiple performance improvements that can result in up to 30%
faster file uploads and 50% faster text field handling.</p>
<ul>
<li>change: Dropped support for Python 3.8 and 3.9 (both EOL), added
support
for Python 3.14</li>
<li>change: Accessing :attr:<code>MultipartSegment.size</code> before
the segment is
complete now raises :exc:<code>ParserStateError</code>. The new
attribute
:attr:<code>MultipartSegment.bytes_received</code> provides the
in-progress segment
body byte count previously exposed by
:attr:<code>MultipartSegment.size</code>.</li>
<li>change: :class:<code>PushMultipartParser</code> now emits
:class:<code>bytes</code> instead of
:class:<code>bytearray</code> for part body chunks. Update your
<code>isinstance</code> checks
if you have any.</li>
<li>change: Enforce <code>part_limit</code> (128 by default) for
url-encoded data in
<code>parse_form_data()</code>. This is consistent with multipart
handling and an
important safeguard against denial of service attacks.</li>
<li>change: New strict-mode check to reject extremely large
boundaries.</li>
<li>change: Raise more helpful :exc:<code>ParserStateError</code>
instead of implicit
:exc:<code>AssertionError</code> or :exc:<code>TypeError</code> when the
parser is used
incorrectly.</li>
<li>change: The boundary delimiter must not appear inside a segment body
(RFC-7578 4.1). This triggers a fatal error now.</li>
<li>feat: Hardened (and faster) header validation.</li>
<li>build: Change default branch to <code>main</code>.</li>
</ul>
<h1>Release 1.3</h1>
<p>This release adds new APIs and contains internal or non-breaking api
changes.</p>
<ul>
<li>feat: New convenience methods
:meth:<code>PushMultipartParser.parse_blocking</code> and
:meth:<code>PushMultipartParser.parse_async</code>.</li>
<li>feat: Nicer error messages when reading from a closed
<code>MultipartPart</code>.</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/defnull/multipart/commit/a14fdd637f1a69b6e8db06603489ac495efd1bb2"><code>a14fdd6</code></a>
Release of 2.0.0</li>
<li><a
href="https://github.com/defnull/multipart/commit/dca7836b27b40aec62cd9efbb0ab2f40be0a8b29"><code>dca7836</code></a>
Prepare 2.0 release</li>
<li><a
href="https://github.com/defnull/multipart/commit/1240e8cb74b2473834b76d6096486ae70e59ba79"><code>1240e8c</code></a>
build: fix venv builder in makefile</li>
<li><a
href="https://github.com/defnull/multipart/commit/ba1d834d3884298e4539b58a8eff2532737bcb71"><code>ba1d834</code></a>
change: MultipartSegment.size now throws if incomplete.</li>
<li><a
href="https://github.com/defnull/multipart/commit/59322fee206c150211f84f72769d20f34acb9666"><code>59322fe</code></a>
build: More github CI checks and fixed makefile</li>
<li><a
href="https://github.com/defnull/multipart/commit/4421f32f1674bd0d0b1dea40ce23c2e6da566307"><code>4421f32</code></a>
Switch to stdlib cached_property</li>
<li><a
href="https://github.com/defnull/multipart/commit/520fdf9d2409958589d61fff621aacd190983930"><code>520fdf9</code></a>
All public APIs should be in <strong>all</strong></li>
<li><a
href="https://github.com/defnull/multipart/commit/42e7ea77db397ef3579c0efa09e5f70c4d2a2721"><code>42e7ea7</code></a>
docs: cleanup</li>
<li><a
href="https://github.com/defnull/multipart/commit/bce8554f7a6b469a75531898f685dcbb3ae70b39"><code>bce8554</code></a>
feat: parse_content_disposition is now a publich API</li>
<li><a
href="https://github.com/defnull/multipart/commit/e1c47df9af65279e0c55acd5605b80d0f1c29ea7"><code>e1c47df</code></a>
Link to defnull.de for benchmarks</li>
<li>Additional commits viewable in <a
href="https://github.com/defnull/multipart/compare/v1.3.1...v2.0.0">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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