Skip to content

Commit ae4346e

Browse files
Merge branch 'release/2.4.1'
2 parents 77dd98e + 725f6ab commit ae4346e

10 files changed

Lines changed: 611 additions & 6 deletions

File tree

docs/api.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,10 @@ Object Proxies
339339
``wrapt.PartialCallableObjectProxy``
340340
A proxy that combines a callable with a set of pre-bound positional
341341
and keyword arguments, analogous to ``functools.partial`` but
342-
implemented as an object proxy.
342+
implemented as an object proxy. The bound arguments are available as
343+
the ``_self_args`` and ``_self_kwargs`` attributes, and
344+
``inspect.signature()`` reports the signature of the callable with
345+
the bound parameters removed, as it does for ``functools.partial``.
343346

344347
``wrapt.partial``
345348
Factory function that returns a ``PartialCallableObjectProxy``,

docs/changes.rst

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,56 @@
11
Release Notes
22
=============
33

4+
Version 2.4.1
5+
-------------
6+
7+
**Bugs Fixed**
8+
9+
* The C extension implementation of ``PartialCallableObjectProxy`` did not
10+
expose the bound positional and keyword arguments supplied when the proxy
11+
was created, whereas the pure Python implementation makes them available
12+
as the ``_self_args`` and ``_self_kwargs`` attributes. The C extension
13+
implementation now provides read only ``_self_args`` and ``_self_kwargs``
14+
attributes so that both implementations behave the same.
15+
16+
* ``inspect.signature()`` applied to a ``PartialCallableObjectProxy`` reported
17+
the full signature of the wrapped callable, including the parameters that
18+
the bound positional and keyword arguments already supply, whereas for
19+
``functools.partial`` those parameters are removed. The proxy did not
20+
define ``__signature__``, so ``inspect`` followed ``__wrapped__`` back to
21+
the callable and reported its signature unchanged. The proxy now provides
22+
``__signature__`` on instances, in both the pure Python and C extension
23+
implementations, giving the same result as for an equivalent
24+
``functools.partial``, including a ``ValueError`` when more positional
25+
arguments are bound than the callable accepts.
26+
27+
This also affected wrapper functions used with ``FunctionWrapper`` and
28+
``@wrapt.decorator``. When a wrapped method is called via its class with
29+
the instance passed explicitly, the wrapper function receives a
30+
``PartialCallableObjectProxy`` with the instance bound, and ``args``
31+
without the instance. A wrapper which bound ``args`` and ``kwargs``
32+
against ``inspect.signature(wrapped)`` would fail for such calls with a
33+
``TypeError`` about a missing argument, while working for calls made via
34+
the instance. The reported signature now omits the bound instance so
35+
the binding succeeds.
36+
37+
The signature of a partial whose wrapped callable is itself an already
38+
bound method is still reported incorrectly, for reasons outside of the
39+
control of ``wrapt``. See the "Known Issues" documentation for details.
40+
41+
* The C extension intercepts the ``__module__`` and ``__doc__`` attributes
42+
by name in its attribute get and set slots so that they are forwarded to
43+
the wrapped object. The name was compared by identity against an interned
44+
string, which relied on the attribute name having been interned. Names
45+
originating from Python source code always are, but a name constructed at
46+
runtime, for example by string concatenation or by decoding, is not, and
47+
for such a name the interception was skipped. Getting the attribute then
48+
returned the value captured when the proxy was created rather than the
49+
current value on the wrapped object, and setting it stored the value on
50+
the proxy rather than the wrapped object. The comparison now falls back to
51+
comparing by value when the identity check fails, guarded by a length
52+
check so that the cost for non matching names is unchanged.
53+
454
Version 2.4.0
555
-------------
656

docs/issues.rst

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1124,3 +1124,63 @@ into an ordinary helper method that the hook calls, or into a
11241124
through normal attribute access and therefore honour decorators applied
11251125
via ``@wrapt.decorator``.
11261126

1127+
1128+
1129+
Signature of PartialCallableObjectProxy over a bound method
1130+
-----------------------------------------------------------
1131+
1132+
``inspect.signature()`` applied to a ``PartialCallableObjectProxy``, or to
1133+
the result of ``wrapt.partial()``, reports the signature of the wrapped
1134+
callable with the bound positional and keyword arguments removed, in the
1135+
same way as it does for ``functools.partial``. There is one case where
1136+
this does not hold, which is when the callable being wrapped is itself an
1137+
already bound method.
1138+
1139+
::
1140+
1141+
import inspect
1142+
import functools
1143+
import wrapt
1144+
1145+
class Database:
1146+
def query(self, sql, *args, timeout=None):
1147+
pass
1148+
1149+
db = Database()
1150+
1151+
inspect.signature(functools.partial(db.query, "SELECT 1"))
1152+
# <Signature (*args, timeout=None)>
1153+
1154+
inspect.signature(wrapt.partial(db.query, "SELECT 1"))
1155+
# <Signature (sql, *args, timeout=None)>
1156+
1157+
For the ``wrapt`` proxy the parameter filled by the bound argument is
1158+
still present. This happens because ``inspect.signature()`` first checks
1159+
whether the object it was given is an instance of ``types.MethodType``,
1160+
before it looks for a ``__signature__`` attribute. An object proxy
1161+
reports the class of the object it wraps, so for a proxy around a bound
1162+
method that check succeeds. ``inspect`` then reads the ``__func__``
1163+
attribute, which the proxy also forwards to the bound method, takes the
1164+
signature of the underlying function, and removes only its first
1165+
parameter. The ``__signature__`` attribute of the proxy, which would
1166+
give the correct result, is never consulted.
1167+
1168+
This cannot be corrected within ``wrapt``. Not reporting the class of the
1169+
wrapped object would break the fundamental contract of an object proxy,
1170+
and returning a different object from ``__func__`` would mislead the
1171+
many other consumers of that attribute in order to satisfy one. The
1172+
appropriate fix is for ``inspect`` to consult ``__signature__`` before
1173+
relying on the class of the object.
1174+
1175+
Partials over a plain function are unaffected, including the partial
1176+
created by ``FunctionWrapper`` when a wrapped method is called via the
1177+
class with the instance passed explicitly, since in that case the
1178+
wrapped callable is the plain function and the instance is one of the
1179+
bound arguments. Where the signature of a partial over a bound method is
1180+
needed, pass the underlying function and the instance instead of the
1181+
bound method.
1182+
1183+
::
1184+
1185+
inspect.signature(wrapt.partial(Database.query, db, "SELECT 1"))
1186+
# <Signature (*args, timeout=None)>

src/wrapt-stubs/__init__.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,10 @@ if sys.version_info >= (3, 10):
317317
# PartialCallableObjectProxy
318318

319319
class PartialCallableObjectProxy(BaseObjectProxy[Callable[..., Any]]):
320+
_self_args: tuple[Any, ...]
321+
_self_kwargs: dict[str, Any]
322+
@property
323+
def __signature__(self) -> Signature: ...
320324
def __init__(
321325
self, func: Callable[..., Any], *args: Any, **kwargs: Any
322326
) -> None: ...

src/wrapt/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def _format_version(parts):
1313
)
1414

1515

16-
__version_info__ = ("2", "4", "0")
16+
__version_info__ = ("2", "4", "1")
1717
__version__ = _format_version(__version_info__)
1818

1919
from .__wrapt__ import (

0 commit comments

Comments
 (0)