Skip to content

Commit 6002b9a

Browse files
Expose bound arguments from C PartialCallableObjectProxy.
The pure Python implementation of PartialCallableObjectProxy stores the positional and keyword arguments supplied at creation as the _self_args and _self_kwargs attributes, but the C extension implementation kept them in struct fields with no way to read them from Python. Attribute lookups were instead forwarded to the wrapped callable and failed. Add read only _self_args and _self_kwargs getters to the C type so both implementations expose the same attributes, declare them in the stubs, and add tests covering both implementations.
1 parent b047b3f commit 6002b9a

4 files changed

Lines changed: 100 additions & 0 deletions

File tree

docs/changes.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ Version 2.4.1
66

77
**Bugs Fixed**
88

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+
916
Version 2.4.0
1017
-------------
1118

src/wrapt-stubs/__init__.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,8 @@ 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]
320322
def __init__(
321323
self, func: Callable[..., Any], *args: Any, **kwargs: Any
322324
) -> None: ...

src/wrapt/_wrappers.c

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3930,11 +3930,50 @@ static PyObject *WraptPartialCallableObjectProxy_call(
39303930

39313931
/* ------------------------------------------------------------------------- */;
39323932

3933+
static PyObject *WraptPartialCallableObjectProxy_get_self_args(
3934+
WraptPartialCallableObjectProxyObject *self, void *closure)
3935+
{
3936+
PyObject *value = NULL;
3937+
3938+
value = wrapt_acquire_field((PyObject *)self, &self->args);
3939+
3940+
if (!value)
3941+
return PyTuple_New(0);
3942+
3943+
return value;
3944+
}
3945+
3946+
/* ------------------------------------------------------------------------- */
3947+
3948+
static PyObject *WraptPartialCallableObjectProxy_get_self_kwargs(
3949+
WraptPartialCallableObjectProxyObject *self, void *closure)
3950+
{
3951+
PyObject *value = NULL;
3952+
3953+
value = wrapt_acquire_field((PyObject *)self, &self->kwargs);
3954+
3955+
if (!value)
3956+
return PyDict_New();
3957+
3958+
return value;
3959+
}
3960+
3961+
/* ------------------------------------------------------------------------- */
3962+
3963+
static PyGetSetDef WraptPartialCallableObjectProxy_getset[] = {
3964+
{"_self_args", (getter)WraptPartialCallableObjectProxy_get_self_args, NULL,
3965+
0},
3966+
{"_self_kwargs", (getter)WraptPartialCallableObjectProxy_get_self_kwargs,
3967+
NULL, 0},
3968+
{NULL},
3969+
};
3970+
39333971
static PyType_Slot WraptPartialCallableObjectProxy_slots[] = {
39343972
{Py_tp_dealloc, WraptPartialCallableObjectProxy_dealloc},
39353973
{Py_tp_call, WraptPartialCallableObjectProxy_call},
39363974
{Py_tp_traverse, WraptPartialCallableObjectProxy_traverse},
39373975
{Py_tp_clear, WraptPartialCallableObjectProxy_clear},
3976+
{Py_tp_getset, WraptPartialCallableObjectProxy_getset},
39383977
{Py_tp_init, WraptPartialCallableObjectProxy_init},
39393978
{Py_tp_new, WraptPartialCallableObjectProxy_new},
39403979
{0, NULL},

tests/core/test_callable_object_proxy.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,58 @@ def func0(*args, **kwargs):
6565

6666
self.assertEqual(partial0(), (args, kwargs))
6767

68+
def test_bound_arguments_no_arguments(self):
69+
def func0(*args, **kwargs):
70+
return (args, kwargs)
71+
72+
partial0 = wrapt.partial(func0)
73+
74+
self.assertEqual(partial0._self_args, ())
75+
self.assertEqual(partial0._self_kwargs, {})
76+
77+
def test_bound_arguments_positional(self):
78+
def func0(*args, **kwargs):
79+
return (args, kwargs)
80+
81+
partial0 = wrapt.partial(func0, 1, 2, 3)
82+
83+
self.assertEqual(partial0._self_args, (1, 2, 3))
84+
self.assertEqual(partial0._self_kwargs, {})
85+
86+
def test_bound_arguments_keyword(self):
87+
def func0(*args, **kwargs):
88+
return (args, kwargs)
89+
90+
partial0 = wrapt.partial(func0, k1=1, k2=2)
91+
92+
self.assertEqual(partial0._self_args, ())
93+
self.assertEqual(partial0._self_kwargs, {"k1": 1, "k2": 2})
94+
95+
def test_bound_arguments_mixed(self):
96+
def func0(*args, **kwargs):
97+
return (args, kwargs)
98+
99+
partial0 = wrapt.PartialCallableObjectProxy(func0, 1, 2, k1=1)
100+
101+
self.assertEqual(partial0._self_args, (1, 2))
102+
self.assertEqual(partial0._self_kwargs, {"k1": 1})
103+
104+
def test_bound_arguments_not_forwarded_to_wrapped(self):
105+
# The attributes must come from the proxy itself and not be
106+
# forwarded to a same named attribute on the wrapped callable.
107+
108+
class Callable:
109+
_self_args = ("wrapped",)
110+
_self_kwargs = {"wrapped": True}
111+
112+
def __call__(self, *args, **kwargs):
113+
return (args, kwargs)
114+
115+
partial0 = wrapt.partial(Callable(), 1, k1=1)
116+
117+
self.assertEqual(partial0._self_args, (1,))
118+
self.assertEqual(partial0._self_kwargs, {"k1": 1})
119+
68120

69121
if __name__ == "__main__":
70122
unittest.main()

0 commit comments

Comments
 (0)