Skip to content

Commit

Permalink
pythongh-119180: Alternative approach to metaclass annotations: never…
Browse files Browse the repository at this point in the history
… use the dict
  • Loading branch information
JelleZijlstra committed Jun 21, 2024
1 parent a2f6f7d commit ccc4ad1
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 113 deletions.
16 changes: 3 additions & 13 deletions Lib/test/test_type_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,20 @@ def test_lazy_create_annotations(self):
# a freshly created type shouldn't have an annotations dict yet.
foo = type("Foo", (), {})
for i in range(3):
self.assertFalse("__annotations__" in foo.__dict__)
d = foo.__annotations__
self.assertTrue("__annotations__" in foo.__dict__)
self.assertEqual(foo.__annotations__, d)
self.assertEqual(foo.__dict__['__annotations__'], d)
del foo.__annotations__

def test_setting_annotations(self):
foo = type("Foo", (), {})
for i in range(3):
self.assertFalse("__annotations__" in foo.__dict__)
d = {'a': int}
foo.__annotations__ = d
self.assertTrue("__annotations__" in foo.__dict__)
self.assertEqual(foo.__annotations__, d)
self.assertEqual(foo.__dict__['__annotations__'], d)
del foo.__annotations__

def test_annotations_getset_raises(self):
# builtin types don't have __annotations__ (yet!)
with self.assertRaises(AttributeError):
print(float.__annotations__)
self.assertEqual(float.__annotations__, {})
with self.assertRaises(TypeError):
float.__annotations__ = {}
with self.assertRaises(TypeError):
Expand All @@ -47,17 +39,15 @@ def test_annotations_getset_raises(self):
foo = type("Foo", (), {})
foo.__annotations__ = {}
del foo.__annotations__
with self.assertRaises(AttributeError):
del foo.__annotations__
del foo.__annotations__

def test_annotations_are_created_correctly(self):
class C:
a:int=3
b:str=4
self.assertEqual(C.__annotations__, {"a": int, "b": str})
self.assertTrue("__annotations__" in C.__dict__)
del C.__annotations__
self.assertFalse("__annotations__" in C.__dict__)
self.assertEqual(C.__annotations__, {})

def test_descriptor_still_works(self):
class C:
Expand Down
224 changes: 124 additions & 100 deletions Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1821,33 +1821,21 @@ type_set_doc(PyTypeObject *type, PyObject *value, void *context)
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;
PyObject *cached_value = type->tp_cache;
// No annotations, or an annotations dict
if (cached_value == NULL || PyDict_CheckExact(cached_value)) {
Py_RETURN_NONE;
}
if (annotate) {
descrgetfunc get = Py_TYPE(annotate)->tp_descr_get;
if (get) {
Py_SETREF(annotate, get(annotate, NULL, (PyObject *)type));
}
else if (PyTuple_CheckExact(cached_value)) {
// (annotate, annotations) tuple
PyObject *annotate = PyTuple_GET_ITEM(cached_value, 0);
return Py_XNewRef(annotate);
}
else {
annotate = Py_None;
int result = PyDict_SetItem(dict, &_Py_ID(__annotate__), annotate);
if (result < 0) {
Py_DECREF(dict);
return NULL;
}
// just annotate
assert(PyCallable_Check(cached_value));
return Py_XNewRef(cached_value);
}
Py_DECREF(dict);
return annotate;
}

static int
Expand All @@ -1863,90 +1851,79 @@ type_set_annotate(PyTypeObject *type, PyObject *value, void *Py_UNUSED(ignored))
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;
PyObject *cached_value = type->tp_cache;
if (PyTuple_CheckExact(cached_value)) {
if (Py_IsNone(value)) {
PyObject *annotations = PyTuple_GET_ITEM(cached_value, 1);
Py_XSETREF(type->tp_cache, Py_NewRef(annotations));
}
else {
Py_XSETREF(type->tp_cache, PyTuple_Pack(2, value, PyTuple_GET_ITEM(cached_value, 1)));
}
}
Py_DECREF(dict);
else if (Py_IsNone(value)) {
Py_CLEAR(type->tp_cache);
}
else {
Py_XSETREF(type->tp_cache, Py_XNewRef(value));
}
PyType_Modified(type);
return 0;
}

static PyObject *
type_get_annotations(PyTypeObject *type, void *context)
{
if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) {
PyErr_Format(PyExc_AttributeError, "type object '%s' has no attribute '__annotations__'", type->tp_name);
return NULL;
}

PyObject *annotations;
PyObject *dict = PyType_GetDict(type);
if (PyDict_GetItemRef(dict, &_Py_ID(__annotations__), &annotations) < 0) {
Py_DECREF(dict);
return NULL;
}
Py_DECREF(dict);
if (annotations) {
descrgetfunc get = Py_TYPE(annotations)->tp_descr_get;
if (get) {
Py_SETREF(annotations, get(annotations, NULL, (PyObject *)type));
}
return annotations;
}

PyObject *cached_value = type->tp_cache;
if (cached_value == NULL) {
PyObject *annotations = PyDict_New();
Py_XSETREF(type->tp_cache, annotations);
return Py_XNewRef(annotations);
}
else if (PyDict_CheckExact(cached_value)) {
return Py_NewRef(cached_value);
}
else if (PyTuple_CheckExact(cached_value)) {
// (annotate, annotations) tuple
PyObject *annotations = PyTuple_GET_ITEM(cached_value, 1);
return Py_XNewRef(annotations);
}
else {
PyObject *annotate = type_get_annotate(type, NULL);
if (annotate == NULL) {
Py_DECREF(dict);
// just annotate
assert(PyCallable_Check(cached_value));
PyObject *one = _PyLong_GetOne();
PyObject *annotations = _PyObject_CallOneArg(cached_value, one);
if (annotations == NULL) {
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);
if (result) {
Py_CLEAR(annotations);
} else {
PyType_Modified(type);
}
if (!PyDict_CheckExact(annotations)) {
PyErr_Format(PyExc_TypeError, "__annotate__ returned non-dict of type '%.100s'",
Py_TYPE(annotations)->tp_name);
Py_DECREF(annotations);
return NULL;
}
Py_XSETREF(type->tp_cache, PyTuple_Pack(2, cached_value, annotations));
return annotations;
}
Py_DECREF(dict);
return annotations;
}

static int
Expand All @@ -1958,34 +1935,36 @@ type_set_annotations(PyTypeObject *type, PyObject *value, void *context)
type->tp_name);
return -1;
}

int result;
PyObject *dict = PyType_GetDict(type);
if (value != NULL) {
/* set */
result = PyDict_SetItem(dict, &_Py_ID(__annotations__), value);
} else {
/* delete */
result = PyDict_Pop(dict, &_Py_ID(__annotations__), NULL);
if (result == 0) {
PyErr_SetString(PyExc_AttributeError, "__annotations__");
Py_DECREF(dict);
return -1;
}
}
if (result < 0) {
if (PyDict_Pop(dict, &_Py_ID(__annotations__), NULL) < 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);

if (value == NULL) {
Py_CLEAR(type->tp_cache);
return 0;
}
if (!PyDict_CheckExact(value)) {
PyErr_SetString(PyExc_TypeError, "__annotations__ must be a dict");
return -1;
}
// setting annotations clears annotate (for now)
Py_XSETREF(type->tp_cache, Py_XNewRef(value));
// PyObject *cached_value = type->tp_cache;
// if (cached_value == NULL || PyDict_CheckExact(cached_value)) {
// Py_XSETREF(type->tp_cache, Py_XNewRef(value));
// }
// else if (PyTuple_CheckExact(cached_value)) {
// // (annotate, annotations) tuple
// Py_XSETREF(type->tp_cache, PyTuple_Pack(2, PyTuple_GET_ITEM(cached_value, 0), value));
// }
// else {
// // just annotate
// assert(PyCallable_Check(cached_value));
// Py_XSETREF(type->tp_cache, PyTuple_Pack(2, cached_value, value));
// }
return 0;
}

Expand Down Expand Up @@ -4225,6 +4204,48 @@ type_new_set_classdictcell(PyTypeObject *type)
return 0;
}

static int
type_new_set_annotate(PyTypeObject *type)
{
PyObject *dict = lookup_tp_dict(type);
PyObject *annotations;
if (PyDict_GetItemRef(dict, &_Py_ID(__annotations__), &annotations) < 0) {
return -1;
}
bool has_annotations = annotations != NULL && PyDict_CheckExact(annotations);
PyObject *annotate;
if (PyDict_GetItemRef(dict, &_Py_ID(__annotate__), &annotate) < 0) {
Py_DECREF(annotations);
return -1;
}
bool has_annotate = annotate != NULL && PyCallable_Check(annotate);
PyObject *value = NULL;
if (has_annotations && has_annotate) {
value = PyTuple_Pack(2, annotations, annotate);
if (value == NULL) {
Py_DECREF(annotations);
Py_DECREF(annotate);
return -1;
}
}
else if (has_annotations) {
value = annotations;
}
else if (has_annotate) {
value = annotate;
}
Py_XSETREF(type->tp_cache, Py_XNewRef(value));
Py_XDECREF(annotations);
Py_XDECREF(annotate);
if (has_annotations && PyDict_DelItem(dict, &_Py_ID(__annotations__)) < 0) {
return -1;
}
if (has_annotate && PyDict_DelItem(dict, &_Py_ID(__annotate__)) < 0) {
return -1;
}
return 0;
}

static int
type_new_set_attrs(const type_new_ctx *ctx, PyTypeObject *type)
{
Expand Down Expand Up @@ -4271,6 +4292,9 @@ type_new_set_attrs(const type_new_ctx *ctx, PyTypeObject *type)
if (type_new_set_classdictcell(type) < 0) {
return -1;
}
if (type_new_set_annotate(type) < 0) {
return -1;
}
return 0;
}

Expand Down

0 comments on commit ccc4ad1

Please sign in to comment.