gh-119180: PEP 649: Add __annotate__ attributes (#119209)

This commit is contained in:
Jelle Zijlstra 2024-05-21 22:38:12 -04:00 committed by GitHub
parent 73ab83b27f
commit e9875ecb5d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 324 additions and 18 deletions

View File

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

View File

@ -590,6 +590,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__all__));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__and__));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__anext__));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__annotate__));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__annotations__));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__args__));
_PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(__asyncio_running_event_loop__));

View File

@ -79,6 +79,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(__all__)
STRUCT_FOR_ID(__and__)
STRUCT_FOR_ID(__anext__)
STRUCT_FOR_ID(__annotate__)
STRUCT_FOR_ID(__annotations__)
STRUCT_FOR_ID(__args__)
STRUCT_FOR_ID(__asyncio_running_event_loop__)

View File

@ -588,6 +588,7 @@ extern "C" {
INIT_ID(__all__), \
INIT_ID(__and__), \
INIT_ID(__anext__), \
INIT_ID(__annotate__), \
INIT_ID(__annotations__), \
INIT_ID(__args__), \
INIT_ID(__asyncio_running_event_loop__), \

View File

@ -78,6 +78,9 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) {
string = &_Py_ID(__anext__);
assert(_PyUnicode_CheckConsistency(string, 1));
_PyUnicode_InternInPlace(interp, &string);
string = &_Py_ID(__annotate__);
assert(_PyUnicode_CheckConsistency(string, 1));
_PyUnicode_InternInPlace(interp, &string);
string = &_Py_ID(__annotations__);
assert(_PyUnicode_CheckConsistency(string, 1));
_PyUnicode_InternInPlace(interp, &string);

View File

@ -1564,7 +1564,7 @@ class SizeofTest(unittest.TestCase):
check(x, size('3Pi2cP7P2ic??2P'))
# function
def func(): pass
check(func, size('15Pi'))
check(func, size('16Pi'))
class c():
@staticmethod
def foo():

View File

@ -1,4 +1,5 @@
import textwrap
import types
import unittest
from test.support import run_code
@ -212,3 +213,46 @@ class TestSetupAnnotations(unittest.TestCase):
case 0:
x: int = 1
""")
class AnnotateTests(unittest.TestCase):
"""See PEP 649."""
def test_manual_annotate(self):
def f():
pass
mod = types.ModuleType("mod")
class X:
pass
for obj in (f, mod, X):
with self.subTest(obj=obj):
self.check_annotations(obj)
def check_annotations(self, f):
self.assertEqual(f.__annotations__, {})
self.assertIs(f.__annotate__, None)
with self.assertRaisesRegex(TypeError, "__annotate__ must be callable or None"):
f.__annotate__ = 42
f.__annotate__ = lambda: 42
with self.assertRaisesRegex(TypeError, r"takes 0 positional arguments but 1 was given"):
print(f.__annotations__)
f.__annotate__ = lambda x: 42
with self.assertRaisesRegex(TypeError, r"__annotate__ returned non-dict of type 'int'"):
print(f.__annotations__)
f.__annotate__ = lambda x: {"x": x}
self.assertEqual(f.__annotations__, {"x": 1})
# Setting annotate to None does not invalidate the cached __annotations__
f.__annotate__ = None
self.assertEqual(f.__annotations__, {"x": 1})
# But setting it to a new callable does
f.__annotate__ = lambda x: {"y": x}
self.assertEqual(f.__annotations__, {"y": 1})
# Setting f.__annotations__ also clears __annotate__
f.__annotations__ = {"z": 43}
self.assertIs(f.__annotate__, None)

View File

@ -3723,7 +3723,7 @@ class ProtocolTests(BaseTestCase):
acceptable_extra_attrs = {
'_is_protocol', '_is_runtime_protocol', '__parameters__',
'__init__', '__annotations__', '__subclasshook__',
'__init__', '__annotations__', '__subclasshook__', '__annotate__',
}
self.assertLessEqual(vars(NonP).keys(), vars(C).keys() | acceptable_extra_attrs)
self.assertLessEqual(

View File

@ -1889,6 +1889,7 @@ _SPECIAL_NAMES = frozenset({
'__init__', '__module__', '__new__', '__slots__',
'__subclasshook__', '__weakref__', '__class_getitem__',
'__match_args__', '__static_attributes__', '__firstlineno__',
'__annotate__',
})
# These special attributes will be not collected as protocol members.

View File

@ -0,0 +1,2 @@
Add an ``__annotate__`` attribute to functions, classes, and modules as part
of :pep:`649`. Patch by Jelle Zijlstra.

View File

@ -3,6 +3,7 @@
#include "Python.h"
#include "pycore_ceval.h" // _PyEval_BuiltinsFromGlobals()
#include "pycore_long.h" // _PyLong_GetOne()
#include "pycore_modsupport.h" // _PyArg_NoKeywords()
#include "pycore_object.h" // _PyObject_GC_UNTRACK()
#include "pycore_pyerrors.h" // _PyErr_Occurred()
@ -124,6 +125,7 @@ _PyFunction_FromConstructor(PyFrameConstructor *constr)
op->func_weakreflist = NULL;
op->func_module = module;
op->func_annotations = NULL;
op->func_annotate = NULL;
op->func_typeparams = NULL;
op->vectorcall = _PyFunction_Vectorcall;
op->func_version = 0;
@ -202,6 +204,7 @@ PyFunction_NewWithQualName(PyObject *code, PyObject *globals, PyObject *qualname
op->func_weakreflist = NULL;
op->func_module = module;
op->func_annotations = NULL;
op->func_annotate = NULL;
op->func_typeparams = NULL;
op->vectorcall = _PyFunction_Vectorcall;
op->func_version = 0;
@ -512,7 +515,22 @@ static PyObject *
func_get_annotation_dict(PyFunctionObject *op)
{
if (op->func_annotations == NULL) {
return NULL;
if (op->func_annotate == NULL || !PyCallable_Check(op->func_annotate)) {
Py_RETURN_NONE;
}
PyObject *one = _PyLong_GetOne();
PyObject *ann_dict = _PyObject_CallOneArg(op->func_annotate, one);
if (ann_dict == NULL) {
return NULL;
}
if (!PyDict_Check(ann_dict)) {
PyErr_Format(PyExc_TypeError, "__annotate__ returned non-dict of type '%.100s'",
Py_TYPE(ann_dict)->tp_name);
Py_DECREF(ann_dict);
return NULL;
}
Py_XSETREF(op->func_annotations, ann_dict);
return ann_dict;
}
if (PyTuple_CheckExact(op->func_annotations)) {
PyObject *ann_tuple = op->func_annotations;
@ -565,7 +583,9 @@ PyFunction_SetAnnotations(PyObject *op, PyObject *annotations)
"non-dict annotations");
return -1;
}
Py_XSETREF(((PyFunctionObject *)op)->func_annotations, annotations);
PyFunctionObject *func = (PyFunctionObject *)op;
Py_XSETREF(func->func_annotations, annotations);
Py_CLEAR(func->func_annotate);
return 0;
}
@ -763,10 +783,44 @@ func_set_kwdefaults(PyFunctionObject *op, PyObject *value, void *Py_UNUSED(ignor
return 0;
}
static PyObject *
func_get_annotate(PyFunctionObject *op, void *Py_UNUSED(ignored))
{
if (op->func_annotate == NULL) {
Py_RETURN_NONE;
}
return Py_NewRef(op->func_annotate);
}
static int
func_set_annotate(PyFunctionObject *op, PyObject *value, void *Py_UNUSED(ignored))
{
if (value == NULL) {
PyErr_SetString(PyExc_TypeError,
"__annotate__ cannot be deleted");
return -1;
}
if (Py_IsNone(value)) {
Py_XSETREF(op->func_annotate, value);
return 0;
}
else if (PyCallable_Check(value)) {
Py_XSETREF(op->func_annotate, Py_XNewRef(value));
Py_CLEAR(op->func_annotations);
return 0;
}
else {
PyErr_SetString(PyExc_TypeError,
"__annotate__ must be callable or None");
return -1;
}
}
static PyObject *
func_get_annotations(PyFunctionObject *op, void *Py_UNUSED(ignored))
{
if (op->func_annotations == NULL) {
if (op->func_annotations == NULL &&
(op->func_annotate == NULL || !PyCallable_Check(op->func_annotate))) {
op->func_annotations = PyDict_New();
if (op->func_annotations == NULL)
return NULL;
@ -789,6 +843,7 @@ func_set_annotations(PyFunctionObject *op, PyObject *value, void *Py_UNUSED(igno
return -1;
}
Py_XSETREF(op->func_annotations, Py_XNewRef(value));
Py_CLEAR(op->func_annotate);
return 0;
}
@ -836,6 +891,7 @@ static PyGetSetDef func_getsetlist[] = {
(setter)func_set_kwdefaults},
{"__annotations__", (getter)func_get_annotations,
(setter)func_set_annotations},
{"__annotate__", (getter)func_get_annotate, (setter)func_set_annotate},
{"__dict__", PyObject_GenericGetDict, PyObject_GenericSetDict},
{"__name__", (getter)func_get_name, (setter)func_set_name},
{"__qualname__", (getter)func_get_qualname, (setter)func_set_qualname},
@ -972,6 +1028,7 @@ func_clear(PyFunctionObject *op)
Py_CLEAR(op->func_dict);
Py_CLEAR(op->func_closure);
Py_CLEAR(op->func_annotations);
Py_CLEAR(op->func_annotate);
Py_CLEAR(op->func_typeparams);
// Don't Py_CLEAR(op->func_code), since code is always required
// to be non-NULL. Similarly, name and qualname shouldn't be NULL.
@ -1028,6 +1085,7 @@ func_traverse(PyFunctionObject *f, visitproc visit, void *arg)
Py_VISIT(f->func_dict);
Py_VISIT(f->func_closure);
Py_VISIT(f->func_annotations);
Py_VISIT(f->func_annotate);
Py_VISIT(f->func_typeparams);
Py_VISIT(f->func_qualname);
return 0;

View File

@ -5,6 +5,7 @@
#include "pycore_call.h" // _PyObject_CallNoArgs()
#include "pycore_fileutils.h" // _Py_wgetcwd
#include "pycore_interp.h" // PyInterpreterState.importlib
#include "pycore_long.h" // _PyLong_GetOne()
#include "pycore_modsupport.h" // _PyModule_CreateInitialized()
#include "pycore_moduleobject.h" // _PyModule_GetDef()
#include "pycore_object.h" // _PyType_AllocNoTrack
@ -1133,7 +1134,7 @@ static PyMethodDef module_methods[] = {
};
static PyObject *
module_get_annotations(PyModuleObject *m, void *Py_UNUSED(ignored))
module_get_dict(PyModuleObject *m)
{
PyObject *dict = PyObject_GetAttr((PyObject *)m, &_Py_ID(__dict__));
if (dict == NULL) {
@ -1144,10 +1145,97 @@ module_get_annotations(PyModuleObject *m, void *Py_UNUSED(ignored))
Py_DECREF(dict);
return NULL;
}
return dict;
}
static PyObject *
module_get_annotate(PyModuleObject *m, void *Py_UNUSED(ignored))
{
PyObject *dict = module_get_dict(m);
if (dict == NULL) {
return NULL;
}
PyObject *annotate;
if (PyDict_GetItemRef(dict, &_Py_ID(__annotate__), &annotate) == 0) {
annotate = Py_None;
if (PyDict_SetItem(dict, &_Py_ID(__annotate__), annotate) == -1) {
Py_CLEAR(annotate);
}
}
Py_DECREF(dict);
return annotate;
}
static int
module_set_annotate(PyModuleObject *m, PyObject *value, void *Py_UNUSED(ignored))
{
if (value == NULL) {
PyErr_SetString(PyExc_TypeError, "cannot delete __annotate__ attribute");
return -1;
}
PyObject *dict = module_get_dict(m);
if (dict == NULL) {
return -1;
}
if (!Py_IsNone(value) && !PyCallable_Check(value)) {
PyErr_SetString(PyExc_TypeError, "__annotate__ must be callable or None");
Py_DECREF(dict);
return -1;
}
if (PyDict_SetItem(dict, &_Py_ID(__annotate__), value) == -1) {
Py_DECREF(dict);
return -1;
}
if (!Py_IsNone(value)) {
if (PyDict_Pop(dict, &_Py_ID(__annotations__), NULL) == -1) {
Py_DECREF(dict);
return -1;
}
}
Py_DECREF(dict);
return 0;
}
static PyObject *
module_get_annotations(PyModuleObject *m, void *Py_UNUSED(ignored))
{
PyObject *dict = module_get_dict(m);
if (dict == NULL) {
return NULL;
}
PyObject *annotations;
if (PyDict_GetItemRef(dict, &_Py_ID(__annotations__), &annotations) == 0) {
annotations = PyDict_New();
PyObject *annotate;
int annotate_result = PyDict_GetItemRef(dict, &_Py_ID(__annotate__), &annotate);
if (annotate_result < 0) {
Py_DECREF(dict);
return NULL;
}
if (annotate_result == 1 && PyCallable_Check(annotate)) {
PyObject *one = _PyLong_GetOne();
annotations = _PyObject_CallOneArg(annotate, one);
if (annotations == NULL) {
Py_DECREF(annotate);
Py_DECREF(dict);
return NULL;
}
if (!PyDict_Check(annotations)) {
PyErr_Format(PyExc_TypeError, "__annotate__ returned non-dict of type '%.100s'",
Py_TYPE(annotations)->tp_name);
Py_DECREF(annotate);
Py_DECREF(annotations);
Py_DECREF(dict);
return NULL;
}
}
else {
annotations = PyDict_New();
}
Py_XDECREF(annotate);
if (annotations) {
int result = PyDict_SetItem(
dict, &_Py_ID(__annotations__), annotations);
@ -1164,14 +1252,10 @@ static int
module_set_annotations(PyModuleObject *m, PyObject *value, void *Py_UNUSED(ignored))
{
int ret = -1;
PyObject *dict = PyObject_GetAttr((PyObject *)m, &_Py_ID(__dict__));
PyObject *dict = module_get_dict(m);
if (dict == NULL) {
return -1;
}
if (!PyDict_Check(dict)) {
PyErr_Format(PyExc_TypeError, "<module>.__dict__ is not a dictionary");
goto exit;
}
if (value != NULL) {
/* set */
@ -1188,8 +1272,10 @@ module_set_annotations(PyModuleObject *m, PyObject *value, void *Py_UNUSED(ignor
ret = 0;
}
}
if (ret == 0 && PyDict_Pop(dict, &_Py_ID(__annotate__), NULL) < 0) {
ret = -1;
}
exit:
Py_DECREF(dict);
return ret;
}
@ -1197,6 +1283,7 @@ exit:
static PyGetSetDef module_getsets[] = {
{"__annotations__", (getter)module_get_annotations, (setter)module_set_annotations},
{"__annotate__", (getter)module_get_annotate, (setter)module_set_annotate},
{NULL}
};

View File

@ -7,7 +7,7 @@
#include "pycore_dict.h" // _PyDict_KeysSize()
#include "pycore_frame.h" // _PyInterpreterFrame
#include "pycore_lock.h" // _PySeqLock_*
#include "pycore_long.h" // _PyLong_IsNegative()
#include "pycore_long.h" // _PyLong_IsNegative(), _PyLong_GetOne()
#include "pycore_memoryobject.h" // _PyMemoryView_FromBufferProc()
#include "pycore_modsupport.h" // _PyArg_NoKwnames()
#include "pycore_moduleobject.h" // _PyModule_GetDef()
@ -1674,6 +1674,76 @@ type_set_doc(PyTypeObject *type, PyObject *value, void *context)
return PyDict_SetItem(dict, &_Py_ID(__doc__), value);
}
static PyObject *
type_get_annotate(PyTypeObject *type, void *Py_UNUSED(ignored))
{
if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) {
PyErr_Format(PyExc_AttributeError, "type object '%s' has no attribute '__annotate__'", type->tp_name);
return NULL;
}
PyObject *annotate;
PyObject *dict = PyType_GetDict(type);
if (PyDict_GetItemRef(dict, &_Py_ID(__annotate__), &annotate) < 0) {
Py_DECREF(dict);
return NULL;
}
if (annotate) {
descrgetfunc get = Py_TYPE(annotate)->tp_descr_get;
if (get) {
Py_SETREF(annotate, get(annotate, NULL, (PyObject *)type));
}
}
else {
annotate = Py_None;
int result = PyDict_SetItem(dict, &_Py_ID(__annotate__), annotate);
if (result < 0) {
Py_DECREF(dict);
return NULL;
}
}
Py_DECREF(dict);
return annotate;
}
static int
type_set_annotate(PyTypeObject *type, PyObject *value, void *Py_UNUSED(ignored))
{
if (value == NULL) {
PyErr_SetString(PyExc_TypeError, "cannot delete __annotate__ attribute");
return -1;
}
if (_PyType_HasFeature(type, Py_TPFLAGS_IMMUTABLETYPE)) {
PyErr_Format(PyExc_TypeError,
"cannot set '__annotate__' attribute of immutable type '%s'",
type->tp_name);
return -1;
}
if (!Py_IsNone(value) && !PyCallable_Check(value)) {
PyErr_SetString(PyExc_TypeError, "__annotate__ must be callable or None");
return -1;
}
PyObject *dict = PyType_GetDict(type);
assert(PyDict_Check(dict));
int result = PyDict_SetItem(dict, &_Py_ID(__annotate__), value);
if (result < 0) {
Py_DECREF(dict);
return -1;
}
if (!Py_IsNone(value)) {
if (PyDict_Pop(dict, &_Py_ID(__annotations__), NULL) == -1) {
Py_DECREF(dict);
PyType_Modified(type);
return -1;
}
}
Py_DECREF(dict);
PyType_Modified(type);
return 0;
}
static PyObject *
type_get_annotations(PyTypeObject *type, void *context)
{
@ -1683,8 +1753,9 @@ type_get_annotations(PyTypeObject *type, void *context)
}
PyObject *annotations;
PyObject *dict = lookup_tp_dict(type);
PyObject *dict = PyType_GetDict(type);
if (PyDict_GetItemRef(dict, &_Py_ID(__annotations__), &annotations) < 0) {
Py_DECREF(dict);
return NULL;
}
if (annotations) {
@ -1694,7 +1765,32 @@ type_get_annotations(PyTypeObject *type, void *context)
}
}
else {
annotations = PyDict_New();
PyObject *annotate = type_get_annotate(type, NULL);
if (annotate == NULL) {
Py_DECREF(dict);
return NULL;
}
if (PyCallable_Check(annotate)) {
PyObject *one = _PyLong_GetOne();
annotations = _PyObject_CallOneArg(annotate, one);
if (annotations == NULL) {
Py_DECREF(dict);
Py_DECREF(annotate);
return NULL;
}
if (!PyDict_Check(annotations)) {
PyErr_Format(PyExc_TypeError, "__annotate__ returned non-dict of type '%.100s'",
Py_TYPE(annotations)->tp_name);
Py_DECREF(annotations);
Py_DECREF(annotate);
Py_DECREF(dict);
return NULL;
}
}
else {
annotations = PyDict_New();
}
Py_DECREF(annotate);
if (annotations) {
int result = PyDict_SetItem(
dict, &_Py_ID(__annotations__), annotations);
@ -1705,6 +1801,7 @@ type_get_annotations(PyTypeObject *type, void *context)
}
}
}
Py_DECREF(dict);
return annotations;
}
@ -1719,7 +1816,7 @@ type_set_annotations(PyTypeObject *type, PyObject *value, void *context)
}
int result;
PyObject *dict = lookup_tp_dict(type);
PyObject *dict = PyType_GetDict(type);
if (value != NULL) {
/* set */
result = PyDict_SetItem(dict, &_Py_ID(__annotations__), value);
@ -1728,14 +1825,23 @@ type_set_annotations(PyTypeObject *type, PyObject *value, void *context)
result = PyDict_Pop(dict, &_Py_ID(__annotations__), NULL);
if (result == 0) {
PyErr_SetString(PyExc_AttributeError, "__annotations__");
Py_DECREF(dict);
return -1;
}
}
if (result < 0) {
Py_DECREF(dict);
return -1;
}
else if (result == 0) {
if (PyDict_Pop(dict, &_Py_ID(__annotate__), NULL) < 0) {
PyType_Modified(type);
Py_DECREF(dict);
return -1;
}
}
PyType_Modified(type);
Py_DECREF(dict);
return 0;
}
@ -1811,6 +1917,7 @@ static PyGetSetDef type_getsets[] = {
{"__doc__", (getter)type_get_doc, (setter)type_set_doc, NULL},
{"__text_signature__", (getter)type_get_text_signature, NULL, NULL},
{"__annotations__", (getter)type_get_annotations, (setter)type_set_annotations, NULL},
{"__annotate__", (getter)type_get_annotate, (setter)type_set_annotate, NULL},
{"__type_params__", (getter)type_get_type_params, (setter)type_set_type_params, NULL},
{0}
};