Skip to content

Commit

Permalink
gh-119180: Lazily wrap annotations on classmethod and staticmethod (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
JelleZijlstra committed May 31, 2024
1 parent 80a4e38 commit d28afd3
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 3 deletions.
38 changes: 36 additions & 2 deletions Lib/test/test_descr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1593,8 +1593,7 @@ def f(cls, arg):
self.fail("classmethod shouldn't accept keyword args")

cm = classmethod(f)
cm_dict = {'__annotations__': {},
'__doc__': (
cm_dict = {'__doc__': (
"f docstring"
if support.HAVE_DOCSTRINGS
else None
Expand All @@ -1610,6 +1609,41 @@ def f(cls, arg):
del cm.x
self.assertNotHasAttr(cm, "x")

def test_classmethod_staticmethod_annotations(self):
for deco in (classmethod, staticmethod):
@deco
def unannotated(cls): pass
@deco
def annotated(cls) -> int: pass

for method in (annotated, unannotated):
with self.subTest(deco=deco, method=method):
original_annotations = dict(method.__wrapped__.__annotations__)
self.assertNotIn('__annotations__', method.__dict__)
self.assertEqual(method.__annotations__, original_annotations)
self.assertIn('__annotations__', method.__dict__)

new_annotations = {"a": "b"}
method.__annotations__ = new_annotations
self.assertEqual(method.__annotations__, new_annotations)
self.assertEqual(method.__wrapped__.__annotations__, original_annotations)

del method.__annotations__
self.assertEqual(method.__annotations__, original_annotations)

original_annotate = method.__wrapped__.__annotate__
self.assertNotIn('__annotate__', method.__dict__)
self.assertIs(method.__annotate__, original_annotate)
self.assertIn('__annotate__', method.__dict__)

new_annotate = lambda: {"annotations": 1}
method.__annotate__ = new_annotate
self.assertIs(method.__annotate__, new_annotate)
self.assertIs(method.__wrapped__.__annotate__, original_annotate)

del method.__annotate__
self.assertIs(method.__annotate__, original_annotate)

@support.refcount_test
def test_refleaks_in_classmethod___init__(self):
gettotalrefcount = support.get_attribute(sys, 'gettotalrefcount')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:func:`classmethod` and :func:`staticmethod` now wrap the
:attr:`__annotations__` and :attr:`!__annotate__` attributes of their
underlying callable lazily. See :pep:`649`. Patch by Jelle Zijlstra.
100 changes: 99 additions & 1 deletion Objects/funcobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1172,12 +1172,57 @@ functools_wraps(PyObject *wrapper, PyObject *wrapped)
COPY_ATTR(__name__);
COPY_ATTR(__qualname__);
COPY_ATTR(__doc__);
COPY_ATTR(__annotations__);
return 0;

#undef COPY_ATTR
}

// Used for wrapping __annotations__ and __annotate__ on classmethod
// and staticmethod objects.
static PyObject *
descriptor_get_wrapped_attribute(PyObject *wrapped, PyObject *dict, PyObject *name)
{
PyObject *res;
if (PyDict_GetItemRef(dict, name, &res) < 0) {
return NULL;
}
if (res != NULL) {
return res;
}
res = PyObject_GetAttr(wrapped, name);
if (res == NULL) {
return NULL;
}
if (PyDict_SetItem(dict, name, res) < 0) {
Py_DECREF(res);
return NULL;
}
return res;
}

static int
descriptor_set_wrapped_attribute(PyObject *dict, PyObject *name, PyObject *value,
char *type_name)
{
if (value == NULL) {
if (PyDict_DelItem(dict, name) < 0) {
if (PyErr_ExceptionMatches(PyExc_KeyError)) {
PyErr_Clear();
PyErr_Format(PyExc_AttributeError,
"'%.200s' object has no attribute '%U'",
type_name, name);
}
else {
return -1;
}
}
return 0;
}
else {
return PyDict_SetItem(dict, name, value);
}
}


/* Class method object */

Expand Down Expand Up @@ -1283,10 +1328,37 @@ cm_get___isabstractmethod__(classmethod *cm, void *closure)
Py_RETURN_FALSE;
}

static PyObject *
cm_get___annotations__(classmethod *cm, void *closure)
{
return descriptor_get_wrapped_attribute(cm->cm_callable, cm->cm_dict, &_Py_ID(__annotations__));
}

static int
cm_set___annotations__(classmethod *cm, PyObject *value, void *closure)
{
return descriptor_set_wrapped_attribute(cm->cm_dict, &_Py_ID(__annotations__), value, "classmethod");
}

static PyObject *
cm_get___annotate__(classmethod *cm, void *closure)
{
return descriptor_get_wrapped_attribute(cm->cm_callable, cm->cm_dict, &_Py_ID(__annotate__));
}

static int
cm_set___annotate__(classmethod *cm, PyObject *value, void *closure)
{
return descriptor_set_wrapped_attribute(cm->cm_dict, &_Py_ID(__annotate__), value, "classmethod");
}


static PyGetSetDef cm_getsetlist[] = {
{"__isabstractmethod__",
(getter)cm_get___isabstractmethod__, NULL, NULL, NULL},
{"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict, NULL, NULL},
{"__annotations__", (getter)cm_get___annotations__, (setter)cm_set___annotations__, NULL, NULL},
{"__annotate__", (getter)cm_get___annotate__, (setter)cm_set___annotate__, NULL, NULL},
{NULL} /* Sentinel */
};

Expand Down Expand Up @@ -1479,10 +1551,36 @@ sm_get___isabstractmethod__(staticmethod *sm, void *closure)
Py_RETURN_FALSE;
}

static PyObject *
sm_get___annotations__(staticmethod *sm, void *closure)
{
return descriptor_get_wrapped_attribute(sm->sm_callable, sm->sm_dict, &_Py_ID(__annotations__));
}

static int
sm_set___annotations__(staticmethod *sm, PyObject *value, void *closure)
{
return descriptor_set_wrapped_attribute(sm->sm_dict, &_Py_ID(__annotations__), value, "staticmethod");
}

static PyObject *
sm_get___annotate__(staticmethod *sm, void *closure)
{
return descriptor_get_wrapped_attribute(sm->sm_callable, sm->sm_dict, &_Py_ID(__annotate__));
}

static int
sm_set___annotate__(staticmethod *sm, PyObject *value, void *closure)
{
return descriptor_set_wrapped_attribute(sm->sm_dict, &_Py_ID(__annotate__), value, "staticmethod");
}

static PyGetSetDef sm_getsetlist[] = {
{"__isabstractmethod__",
(getter)sm_get___isabstractmethod__, NULL, NULL, NULL},
{"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict, NULL, NULL},
{"__annotations__", (getter)sm_get___annotations__, (setter)sm_set___annotations__, NULL, NULL},
{"__annotate__", (getter)sm_get___annotate__, (setter)sm_set___annotate__, NULL, NULL},
{NULL} /* Sentinel */
};

Expand Down

0 comments on commit d28afd3

Please sign in to comment.