Skip to content

Commit e9875ec

Browse files
gh-119180: PEP 649: Add __annotate__ attributes (#119209)
1 parent 73ab83b commit e9875ec

13 files changed

+324
-18
lines changed

Include/cpython/funcobject.h

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ typedef struct {
4141
PyObject *func_weakreflist; /* List of weak references */
4242
PyObject *func_module; /* The __module__ attribute, can be anything */
4343
PyObject *func_annotations; /* Annotations, a dict or NULL */
44+
PyObject *func_annotate; /* Callable to fill the annotations dictionary */
4445
PyObject *func_typeparams; /* Tuple of active type variables or NULL */
4546
vectorcallfunc vectorcall;
4647
/* Version number for use by specializer.

Include/internal/pycore_global_objects_fini_generated.h

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ struct _Py_global_strings {
7979
STRUCT_FOR_ID(__all__)
8080
STRUCT_FOR_ID(__and__)
8181
STRUCT_FOR_ID(__anext__)
82+
STRUCT_FOR_ID(__annotate__)
8283
STRUCT_FOR_ID(__annotations__)
8384
STRUCT_FOR_ID(__args__)
8485
STRUCT_FOR_ID(__asyncio_running_event_loop__)

Include/internal/pycore_runtime_init_generated.h

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_sys.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1564,7 +1564,7 @@ def func():
15641564
check(x, size('3Pi2cP7P2ic??2P'))
15651565
# function
15661566
def func(): pass
1567-
check(func, size('15Pi'))
1567+
check(func, size('16Pi'))
15681568
class c():
15691569
@staticmethod
15701570
def foo():

Lib/test/test_type_annotations.py

+44
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import textwrap
2+
import types
23
import unittest
34
from test.support import run_code
45

@@ -212,3 +213,46 @@ def test_match(self):
212213
case 0:
213214
x: int = 1
214215
""")
216+
217+
218+
class AnnotateTests(unittest.TestCase):
219+
"""See PEP 649."""
220+
def test_manual_annotate(self):
221+
def f():
222+
pass
223+
mod = types.ModuleType("mod")
224+
class X:
225+
pass
226+
227+
for obj in (f, mod, X):
228+
with self.subTest(obj=obj):
229+
self.check_annotations(obj)
230+
231+
def check_annotations(self, f):
232+
self.assertEqual(f.__annotations__, {})
233+
self.assertIs(f.__annotate__, None)
234+
235+
with self.assertRaisesRegex(TypeError, "__annotate__ must be callable or None"):
236+
f.__annotate__ = 42
237+
f.__annotate__ = lambda: 42
238+
with self.assertRaisesRegex(TypeError, r"takes 0 positional arguments but 1 was given"):
239+
print(f.__annotations__)
240+
241+
f.__annotate__ = lambda x: 42
242+
with self.assertRaisesRegex(TypeError, r"__annotate__ returned non-dict of type 'int'"):
243+
print(f.__annotations__)
244+
245+
f.__annotate__ = lambda x: {"x": x}
246+
self.assertEqual(f.__annotations__, {"x": 1})
247+
248+
# Setting annotate to None does not invalidate the cached __annotations__
249+
f.__annotate__ = None
250+
self.assertEqual(f.__annotations__, {"x": 1})
251+
252+
# But setting it to a new callable does
253+
f.__annotate__ = lambda x: {"y": x}
254+
self.assertEqual(f.__annotations__, {"y": 1})
255+
256+
# Setting f.__annotations__ also clears __annotate__
257+
f.__annotations__ = {"z": 43}
258+
self.assertIs(f.__annotate__, None)

Lib/test/test_typing.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3723,7 +3723,7 @@ def meth(self): pass
37233723

37243724
acceptable_extra_attrs = {
37253725
'_is_protocol', '_is_runtime_protocol', '__parameters__',
3726-
'__init__', '__annotations__', '__subclasshook__',
3726+
'__init__', '__annotations__', '__subclasshook__', '__annotate__',
37273727
}
37283728
self.assertLessEqual(vars(NonP).keys(), vars(C).keys() | acceptable_extra_attrs)
37293729
self.assertLessEqual(

Lib/typing.py

+1
Original file line numberDiff line numberDiff line change
@@ -1889,6 +1889,7 @@ class _TypingEllipsis:
18891889
'__init__', '__module__', '__new__', '__slots__',
18901890
'__subclasshook__', '__weakref__', '__class_getitem__',
18911891
'__match_args__', '__static_attributes__', '__firstlineno__',
1892+
'__annotate__',
18921893
})
18931894

18941895
# These special attributes will be not collected as protocol members.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add an ``__annotate__`` attribute to functions, classes, and modules as part
2+
of :pep:`649`. Patch by Jelle Zijlstra.

Objects/funcobject.c

+61-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include "Python.h"
55
#include "pycore_ceval.h" // _PyEval_BuiltinsFromGlobals()
6+
#include "pycore_long.h" // _PyLong_GetOne()
67
#include "pycore_modsupport.h" // _PyArg_NoKeywords()
78
#include "pycore_object.h" // _PyObject_GC_UNTRACK()
89
#include "pycore_pyerrors.h" // _PyErr_Occurred()
@@ -124,6 +125,7 @@ _PyFunction_FromConstructor(PyFrameConstructor *constr)
124125
op->func_weakreflist = NULL;
125126
op->func_module = module;
126127
op->func_annotations = NULL;
128+
op->func_annotate = NULL;
127129
op->func_typeparams = NULL;
128130
op->vectorcall = _PyFunction_Vectorcall;
129131
op->func_version = 0;
@@ -202,6 +204,7 @@ PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname
202204
op->func_weakreflist = NULL;
203205
op->func_module = module;
204206
op->func_annotations = NULL;
207+
op->func_annotate = NULL;
205208
op->func_typeparams = NULL;
206209
op->vectorcall = _PyFunction_Vectorcall;
207210
op->func_version = 0;
@@ -512,7 +515,22 @@ static PyObject *
512515
func_get_annotation_dict(PyFunctionObject *op)
513516
{
514517
if (op->func_annotations == NULL) {
515-
return NULL;
518+
if (op->func_annotate == NULL || !PyCallable_Check(op->func_annotate)) {
519+
Py_RETURN_NONE;
520+
}
521+
PyObject *one = _PyLong_GetOne();
522+
PyObject *ann_dict = _PyObject_CallOneArg(op->func_annotate, one);
523+
if (ann_dict == NULL) {
524+
return NULL;
525+
}
526+
if (!PyDict_Check(ann_dict)) {
527+
PyErr_Format(PyExc_TypeError, "__annotate__ returned non-dict of type '%.100s'",
528+
Py_TYPE(ann_dict)->tp_name);
529+
Py_DECREF(ann_dict);
530+
return NULL;
531+
}
532+
Py_XSETREF(op->func_annotations, ann_dict);
533+
return ann_dict;
516534
}
517535
if (PyTuple_CheckExact(op->func_annotations)) {
518536
PyObject *ann_tuple = op->func_annotations;
@@ -565,7 +583,9 @@ PyFunction_SetAnnotations(PyObject *op, PyObject *annotations)
565583
"non-dict annotations");
566584
return -1;
567585
}
568-
Py_XSETREF(((PyFunctionObject *)op)->func_annotations, annotations);
586+
PyFunctionObject *func = (PyFunctionObject *)op;
587+
Py_XSETREF(func->func_annotations, annotations);
588+
Py_CLEAR(func->func_annotate);
569589
return 0;
570590
}
571591

@@ -763,10 +783,44 @@ func_set_kwdefaults(PyFunctionObject *op, PyObject *value, void *Py_UNUSED(ignor
763783
return 0;
764784
}
765785

786+
static PyObject *
787+
func_get_annotate(PyFunctionObject *op, void *Py_UNUSED(ignored))
788+
{
789+
if (op->func_annotate == NULL) {
790+
Py_RETURN_NONE;
791+
}
792+
return Py_NewRef(op->func_annotate);
793+
}
794+
795+
static int
796+
func_set_annotate(PyFunctionObject *op, PyObject *value, void *Py_UNUSED(ignored))
797+
{
798+
if (value == NULL) {
799+
PyErr_SetString(PyExc_TypeError,
800+
"__annotate__ cannot be deleted");
801+
return -1;
802+
}
803+
if (Py_IsNone(value)) {
804+
Py_XSETREF(op->func_annotate, value);
805+
return 0;
806+
}
807+
else if (PyCallable_Check(value)) {
808+
Py_XSETREF(op->func_annotate, Py_XNewRef(value));
809+
Py_CLEAR(op->func_annotations);
810+
return 0;
811+
}
812+
else {
813+
PyErr_SetString(PyExc_TypeError,
814+
"__annotate__ must be callable or None");
815+
return -1;
816+
}
817+
}
818+
766819
static PyObject *
767820
func_get_annotations(PyFunctionObject *op, void *Py_UNUSED(ignored))
768821
{
769-
if (op->func_annotations == NULL) {
822+
if (op->func_annotations == NULL &&
823+
(op->func_annotate == NULL || !PyCallable_Check(op->func_annotate))) {
770824
op->func_annotations = PyDict_New();
771825
if (op->func_annotations == NULL)
772826
return NULL;
@@ -789,6 +843,7 @@ func_set_annotations(PyFunctionObject *op, PyObject *value, void *Py_UNUSED(igno
789843
return -1;
790844
}
791845
Py_XSETREF(op->func_annotations, Py_XNewRef(value));
846+
Py_CLEAR(op->func_annotate);
792847
return 0;
793848
}
794849

@@ -836,6 +891,7 @@ static PyGetSetDef func_getsetlist[] = {
836891
(setter)func_set_kwdefaults},
837892
{"__annotations__", (getter)func_get_annotations,
838893
(setter)func_set_annotations},
894+
{"__annotate__", (getter)func_get_annotate, (setter)func_set_annotate},
839895
{"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict},
840896
{"__name__", (getter)func_get_name, (setter)func_set_name},
841897
{"__qualname__", (getter)func_get_qualname, (setter)func_set_qualname},
@@ -972,6 +1028,7 @@ func_clear(PyFunctionObject *op)
9721028
Py_CLEAR(op->func_dict);
9731029
Py_CLEAR(op->func_closure);
9741030
Py_CLEAR(op->func_annotations);
1031+
Py_CLEAR(op->func_annotate);
9751032
Py_CLEAR(op->func_typeparams);
9761033
// Don't Py_CLEAR(op->func_code), since code is always required
9771034
// to be non-NULL. Similarly, name and qualname shouldn't be NULL.
@@ -1028,6 +1085,7 @@ func_traverse(PyFunctionObject *f, visitproc visit, void *arg)
10281085
Py_VISIT(f->func_dict);
10291086
Py_VISIT(f->func_closure);
10301087
Py_VISIT(f->func_annotations);
1088+
Py_VISIT(f->func_annotate);
10311089
Py_VISIT(f->func_typeparams);
10321090
Py_VISIT(f->func_qualname);
10331091
return 0;

0 commit comments

Comments
 (0)