Skip to content

Commit d28afd3

Browse files
gh-119180: Lazily wrap annotations on classmethod and staticmethod (#119864)
1 parent 80a4e38 commit d28afd3

File tree

3 files changed

+138
-3
lines changed

3 files changed

+138
-3
lines changed

Lib/test/test_descr.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1593,8 +1593,7 @@ def f(cls, arg):
15931593
self.fail("classmethod shouldn't accept keyword args")
15941594

15951595
cm = classmethod(f)
1596-
cm_dict = {'__annotations__': {},
1597-
'__doc__': (
1596+
cm_dict = {'__doc__': (
15981597
"f docstring"
15991598
if support.HAVE_DOCSTRINGS
16001599
else None
@@ -1610,6 +1609,41 @@ def f(cls, arg):
16101609
del cm.x
16111610
self.assertNotHasAttr(cm, "x")
16121611

1612+
def test_classmethod_staticmethod_annotations(self):
1613+
for deco in (classmethod, staticmethod):
1614+
@deco
1615+
def unannotated(cls): pass
1616+
@deco
1617+
def annotated(cls) -> int: pass
1618+
1619+
for method in (annotated, unannotated):
1620+
with self.subTest(deco=deco, method=method):
1621+
original_annotations = dict(method.__wrapped__.__annotations__)
1622+
self.assertNotIn('__annotations__', method.__dict__)
1623+
self.assertEqual(method.__annotations__, original_annotations)
1624+
self.assertIn('__annotations__', method.__dict__)
1625+
1626+
new_annotations = {"a": "b"}
1627+
method.__annotations__ = new_annotations
1628+
self.assertEqual(method.__annotations__, new_annotations)
1629+
self.assertEqual(method.__wrapped__.__annotations__, original_annotations)
1630+
1631+
del method.__annotations__
1632+
self.assertEqual(method.__annotations__, original_annotations)
1633+
1634+
original_annotate = method.__wrapped__.__annotate__
1635+
self.assertNotIn('__annotate__', method.__dict__)
1636+
self.assertIs(method.__annotate__, original_annotate)
1637+
self.assertIn('__annotate__', method.__dict__)
1638+
1639+
new_annotate = lambda: {"annotations": 1}
1640+
method.__annotate__ = new_annotate
1641+
self.assertIs(method.__annotate__, new_annotate)
1642+
self.assertIs(method.__wrapped__.__annotate__, original_annotate)
1643+
1644+
del method.__annotate__
1645+
self.assertIs(method.__annotate__, original_annotate)
1646+
16131647
@support.refcount_test
16141648
def test_refleaks_in_classmethod___init__(self):
16151649
gettotalrefcount = support.get_attribute(sys, 'gettotalrefcount')
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:func:`classmethod` and :func:`staticmethod` now wrap the
2+
:attr:`__annotations__` and :attr:`!__annotate__` attributes of their
3+
underlying callable lazily. See :pep:`649`. Patch by Jelle Zijlstra.

Objects/funcobject.c

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1172,12 +1172,57 @@ functools_wraps(PyObject *wrapper, PyObject *wrapped)
11721172
COPY_ATTR(__name__);
11731173
COPY_ATTR(__qualname__);
11741174
COPY_ATTR(__doc__);
1175-
COPY_ATTR(__annotations__);
11761175
return 0;
11771176

11781177
#undef COPY_ATTR
11791178
}
11801179

1180+
// Used for wrapping __annotations__ and __annotate__ on classmethod
1181+
// and staticmethod objects.
1182+
static PyObject *
1183+
descriptor_get_wrapped_attribute(PyObject *wrapped, PyObject *dict, PyObject *name)
1184+
{
1185+
PyObject *res;
1186+
if (PyDict_GetItemRef(dict, name, &res) < 0) {
1187+
return NULL;
1188+
}
1189+
if (res != NULL) {
1190+
return res;
1191+
}
1192+
res = PyObject_GetAttr(wrapped, name);
1193+
if (res == NULL) {
1194+
return NULL;
1195+
}
1196+
if (PyDict_SetItem(dict, name, res) < 0) {
1197+
Py_DECREF(res);
1198+
return NULL;
1199+
}
1200+
return res;
1201+
}
1202+
1203+
static int
1204+
descriptor_set_wrapped_attribute(PyObject *dict, PyObject *name, PyObject *value,
1205+
char *type_name)
1206+
{
1207+
if (value == NULL) {
1208+
if (PyDict_DelItem(dict, name) < 0) {
1209+
if (PyErr_ExceptionMatches(PyExc_KeyError)) {
1210+
PyErr_Clear();
1211+
PyErr_Format(PyExc_AttributeError,
1212+
"'%.200s' object has no attribute '%U'",
1213+
type_name, name);
1214+
}
1215+
else {
1216+
return -1;
1217+
}
1218+
}
1219+
return 0;
1220+
}
1221+
else {
1222+
return PyDict_SetItem(dict, name, value);
1223+
}
1224+
}
1225+
11811226

11821227
/* Class method object */
11831228

@@ -1283,10 +1328,37 @@ cm_get___isabstractmethod__(classmethod *cm, void *closure)
12831328
Py_RETURN_FALSE;
12841329
}
12851330

1331+
static PyObject *
1332+
cm_get___annotations__(classmethod *cm, void *closure)
1333+
{
1334+
return descriptor_get_wrapped_attribute(cm->cm_callable, cm->cm_dict, &_Py_ID(__annotations__));
1335+
}
1336+
1337+
static int
1338+
cm_set___annotations__(classmethod *cm, PyObject *value, void *closure)
1339+
{
1340+
return descriptor_set_wrapped_attribute(cm->cm_dict, &_Py_ID(__annotations__), value, "classmethod");
1341+
}
1342+
1343+
static PyObject *
1344+
cm_get___annotate__(classmethod *cm, void *closure)
1345+
{
1346+
return descriptor_get_wrapped_attribute(cm->cm_callable, cm->cm_dict, &_Py_ID(__annotate__));
1347+
}
1348+
1349+
static int
1350+
cm_set___annotate__(classmethod *cm, PyObject *value, void *closure)
1351+
{
1352+
return descriptor_set_wrapped_attribute(cm->cm_dict, &_Py_ID(__annotate__), value, "classmethod");
1353+
}
1354+
1355+
12861356
static PyGetSetDef cm_getsetlist[] = {
12871357
{"__isabstractmethod__",
12881358
(getter)cm_get___isabstractmethod__, NULL, NULL, NULL},
12891359
{"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict, NULL, NULL},
1360+
{"__annotations__", (getter)cm_get___annotations__, (setter)cm_set___annotations__, NULL, NULL},
1361+
{"__annotate__", (getter)cm_get___annotate__, (setter)cm_set___annotate__, NULL, NULL},
12901362
{NULL} /* Sentinel */
12911363
};
12921364

@@ -1479,10 +1551,36 @@ sm_get___isabstractmethod__(staticmethod *sm, void *closure)
14791551
Py_RETURN_FALSE;
14801552
}
14811553

1554+
static PyObject *
1555+
sm_get___annotations__(staticmethod *sm, void *closure)
1556+
{
1557+
return descriptor_get_wrapped_attribute(sm->sm_callable, sm->sm_dict, &_Py_ID(__annotations__));
1558+
}
1559+
1560+
static int
1561+
sm_set___annotations__(staticmethod *sm, PyObject *value, void *closure)
1562+
{
1563+
return descriptor_set_wrapped_attribute(sm->sm_dict, &_Py_ID(__annotations__), value, "staticmethod");
1564+
}
1565+
1566+
static PyObject *
1567+
sm_get___annotate__(staticmethod *sm, void *closure)
1568+
{
1569+
return descriptor_get_wrapped_attribute(sm->sm_callable, sm->sm_dict, &_Py_ID(__annotate__));
1570+
}
1571+
1572+
static int
1573+
sm_set___annotate__(staticmethod *sm, PyObject *value, void *closure)
1574+
{
1575+
return descriptor_set_wrapped_attribute(sm->sm_dict, &_Py_ID(__annotate__), value, "staticmethod");
1576+
}
1577+
14821578
static PyGetSetDef sm_getsetlist[] = {
14831579
{"__isabstractmethod__",
14841580
(getter)sm_get___isabstractmethod__, NULL, NULL, NULL},
14851581
{"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict, NULL, NULL},
1582+
{"__annotations__", (getter)sm_get___annotations__, (setter)sm_set___annotations__, NULL, NULL},
1583+
{"__annotate__", (getter)sm_get___annotate__, (setter)sm_set___annotate__, NULL, NULL},
14861584
{NULL} /* Sentinel */
14871585
};
14881586

0 commit comments

Comments
 (0)