Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-119180: Lazily wrap annotations on classmethod and staticmethod #119864

Merged
merged 1 commit into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading