Skip to content

Commit

Permalink
Propogate __set_name__() call to descriptors.
Browse files Browse the repository at this point in the history
  • Loading branch information
GrahamDumpleton committed Aug 5, 2021
1 parent fea6a0a commit e57280a
Show file tree
Hide file tree
Showing 4 changed files with 98 additions and 4 deletions.
9 changes: 9 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ Version 1.12.2
string. In the latter case only Python builtin types can be referenced
in annotations.

* When a decorator was applied on top of a data/non-data descriptor in a
class definition, the call to the special method ``__set_name__()`` to
notify the descriptor of the variable name was not being propogated. Note
that this issue has been addressed in the ``FunctionWrapper`` used by
``@wrapt.decorator`` but has not been applied to the generic
``ObjectProxy`` class. If using ``ObjectProxy`` directly to construct a
custom wrapper which is applied to a descriptor, you will need to
propogate the ``__set_name__()`` call yourself if required.

Version 1.12.1
--------------

Expand Down
45 changes: 41 additions & 4 deletions src/wrapt/_wrappers.c
Original file line number Diff line number Diff line change
Expand Up @@ -2337,11 +2337,11 @@ static PyObject *WraptFunctionWrapperBase_call(
kwds = param_kwds;
}

if (self->instance == Py_None && (self->binding == function_str ||
if ((self->instance == Py_None) && (self->binding == function_str ||
PyObject_RichCompareBool(self->binding, function_str,
Py_EQ) == 1) || self->binding == classmethod_str ||
PyObject_RichCompareBool(self->binding, classmethod_str,
Py_EQ) == 1) {
(PyObject_RichCompareBool(self->binding, classmethod_str,
Py_EQ) == 1)) {

PyObject *instance = NULL;

Expand Down Expand Up @@ -2515,6 +2515,35 @@ static PyObject *WraptFunctionWrapperBase_descr_get(

/* ------------------------------------------------------------------------- */

static PyObject *WraptFunctionWrapperBase_set_name(
WraptFunctionWrapperObject *self, PyObject *args, PyObject *kwds)
{
PyObject *method = NULL;
PyObject *result = NULL;

if (!self->object_proxy.wrapped) {
PyErr_SetString(PyExc_ValueError, "wrapper has not been initialized");
return NULL;
}

method = PyObject_GetAttrString(self->object_proxy.wrapped,
"__set_name__");

if (!method) {
PyErr_Clear();
Py_INCREF(Py_None);
return Py_None;
}

result = PyObject_Call(method, args, kwds);

Py_DECREF(method);

return result;
}

/* ------------------------------------------------------------------------- */

static PyObject *WraptFunctionWrapperBase_get_self_instance(
WraptFunctionWrapperObject *self, void *closure)
{
Expand Down Expand Up @@ -2585,6 +2614,14 @@ static PyObject *WraptFunctionWrapperBase_get_self_parent(

/* ------------------------------------------------------------------------- */;

static PyMethodDef WraptFunctionWrapperBase_methods[] = {
{ "__set_name__", (PyCFunction)WraptFunctionWrapperBase_set_name,
METH_VARARGS | METH_KEYWORDS, 0 },
{ NULL, NULL },
};

/* ------------------------------------------------------------------------- */;

static PyGetSetDef WraptFunctionWrapperBase_getset[] = {
{ "__module__", (getter)WraptObjectProxy_get_module,
(setter)WraptObjectProxy_set_module, 0 },
Expand Down Expand Up @@ -2638,7 +2675,7 @@ PyTypeObject WraptFunctionWrapperBase_Type = {
offsetof(WraptObjectProxyObject, weakreflist), /*tp_weaklistoffset*/
0, /*tp_iter*/
0, /*tp_iternext*/
0, /*tp_methods*/
WraptFunctionWrapperBase_methods, /*tp_methods*/
0, /*tp_members*/
WraptFunctionWrapperBase_getset, /*tp_getset*/
0, /*tp_base*/
Expand Down
10 changes: 10 additions & 0 deletions src/wrapt/wrappers.py
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,16 @@ def __call__(self, *args, **kwargs):
return self._self_wrapper(self.__wrapped__, self._self_instance,
args, kwargs)

def __set_name__(self, owner, name):
# This is a special method use to supply information to
# descriptors about what the name of variable in a class
# definition is. Not wanting to add this to ObjectProxy as not
# sure of broader implications of doing that. Thus restrict to
# FunctionWrapper used by decorators.

if hasattr(self.__wrapped__, "__set_name__"):
self.__wrapped__.__set_name__(owner, name)

class BoundFunctionWrapper(_FunctionWrapperBase):

def __call__(self, *args, **kwargs):
Expand Down
38 changes: 38 additions & 0 deletions tests/test_descriptors_py3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from __future__ import print_function

import unittest

import wrapt

class TestObjectDescriptors(unittest.TestCase):

def test_set_name(self):
@wrapt.decorator
def _decorator(wrapped, instance, args, kwargs):
return wrapped(*args, **kwargs)

attribute_name = []

class _descriptor_wrapper:
def __init__(self, descriptor):
self.__wrapped__ = descriptor

def __set_name__(self, owner, name):
attribute_name.append(name)

def __get__(self, instance, owner=None):
return self.__wrapped__.__get__(instance, owner)

class Instance(object):
@_decorator
@_descriptor_wrapper
def method(self):
return True

instance = Instance()

self.assertEqual(attribute_name, ["method"])
self.assertEqual(instance.method(), True)

if __name__ == '__main__':
unittest.main()

0 comments on commit e57280a

Please sign in to comment.