gh-132426: Add get_annotate_from_class_namespace replacing get_annotate_function by JelleZijlstra · Pull Request #132490 · python/cpython · GitHub
Skip to content
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
89 changes: 80 additions & 9 deletions Doc/library/annotationlib.rst
19 changes: 3 additions & 16 deletions Doc/reference/datamodel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1228,15 +1228,9 @@ Special attributes
:attr:`__annotations__ attributes <object.__annotations__>`.

For best practices on working with :attr:`~object.__annotations__`,
please see :mod:`annotationlib`.

.. caution::

Accessing the :attr:`!__annotations__` attribute of a class
object directly may yield incorrect results in the presence of
metaclasses. In addition, the attribute may not exist for
some classes. Use :func:`annotationlib.get_annotations` to
retrieve class annotations safely.
please see :mod:`annotationlib`. Where possible, use
:func:`annotationlib.get_annotations` instead of accessing this
attribute directly.

.. versionchanged:: 3.14
Annotations are now :ref:`lazily evaluated <lazy-evaluation>`.
Expand All @@ -1247,13 +1241,6 @@ Special attributes
if the class has no annotations.
See also: :attr:`__annotate__ attributes <object.__annotate__>`.

.. caution::

Accessing the :attr:`!__annotate__` attribute of a class
object directly may yield incorrect results in the presence of
metaclasses. Use :func:`annotationlib.get_annotate_function` to
retrieve the annotate function safely.

.. versionadded:: 3.14

* - .. attribute:: type.__type_params__
Expand Down
24 changes: 10 additions & 14 deletions Lib/annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"ForwardRef",
"call_annotate_function",
"call_evaluate_function",
"get_annotate_function",
"get_annotate_from_class_namespace",
"get_annotations",
"annotations_to_string",
"type_repr",
Expand Down Expand Up @@ -619,20 +619,16 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
raise ValueError(f"Invalid format: {format!r}")


def get_annotate_function(obj):
"""Get the __annotate__ function for an object.
def get_annotate_from_class_namespace(obj):
"""Retrieve the annotate function from a class namespace dictionary.

obj may be a function, class, or module, or a user-defined type with
an `__annotate__` attribute.

Returns the __annotate__ function or None.
Return None if the namespace does not contain an annotate function.
This is useful in metaclass ``__new__`` methods to retrieve the annotate function.
"""
if isinstance(obj, dict):
try:
return obj["__annotate__"]
except KeyError:
return obj.get("__annotate_func__", None)
return getattr(obj, "__annotate__", None)
try:
return obj["__annotate__"]
except KeyError:
return obj.get("__annotate_func__", None)


def get_annotations(
Expand Down Expand Up @@ -832,7 +828,7 @@ def _get_and_call_annotate(obj, format):

May not return a fresh dictionary.
"""
annotate = get_annotate_function(obj)
annotate = getattr(obj, "__annotate__", None)
if annotate is not None:
ann = call_annotate_function(annotate, format, owner=obj)
if not isinstance(ann, dict):
Expand Down
60 changes: 37 additions & 23 deletions Lib/test/test_annotationlib.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Tests for the annotations module."""

import textwrap
import annotationlib
import builtins
import collections
Expand All @@ -12,7 +13,6 @@
Format,
ForwardRef,
get_annotations,
get_annotate_function,
annotations_to_string,
type_repr,
)
Expand Down Expand Up @@ -1085,13 +1085,13 @@ class Y(metaclass=Meta):
b: float

self.assertEqual(get_annotations(Meta), {"a": int})
self.assertEqual(get_annotate_function(Meta)(Format.VALUE), {"a": int})
self.assertEqual(Meta.__annotate__(Format.VALUE), {"a": int})

self.assertEqual(get_annotations(X), {})
self.assertIs(get_annotate_function(X), None)
self.assertIs(X.__annotate__, None)

self.assertEqual(get_annotations(Y), {"b": float})
self.assertEqual(get_annotate_function(Y)(Format.VALUE), {"b": float})
self.assertEqual(Y.__annotate__(Format.VALUE), {"b": float})

def test_unannotated_meta(self):
class Meta(type):
Expand All @@ -1104,13 +1104,13 @@ class Y(X):
pass

self.assertEqual(get_annotations(Meta), {})
self.assertIs(get_annotate_function(Meta), None)
self.assertIs(Meta.__annotate__, None)

self.assertEqual(get_annotations(Y), {})
self.assertIs(get_annotate_function(Y), None)
self.assertIs(Y.__annotate__, None)

self.assertEqual(get_annotations(X), {"a": str})
self.assertEqual(get_annotate_function(X)(Format.VALUE), {"a": str})
self.assertEqual(X.__annotate__(Format.VALUE), {"a": str})

def test_ordering(self):
# Based on a sample by David Ellis
Expand Down Expand Up @@ -1148,7 +1148,7 @@ class D(metaclass=Meta):
for c in classes:
with self.subTest(c=c):
self.assertEqual(get_annotations(c), c.expected_annotations)
annotate_func = get_annotate_function(c)
annotate_func = getattr(c, "__annotate__", None)
if c.expected_annotations:
self.assertEqual(
annotate_func(Format.VALUE), c.expected_annotations
Expand All @@ -1157,25 +1157,39 @@ class D(metaclass=Meta):
self.assertIs(annotate_func, None)


class TestGetAnnotateFunction(unittest.TestCase):
def test_static_class(self):
self.assertIsNone(get_annotate_function(object))
self.assertIsNone(get_annotate_function(int))

def test_unannotated_class(self):
class C:
pass
class TestGetAnnotateFromClassNamespace(unittest.TestCase):
def test_with_metaclass(self):
class Meta(type):
def __new__(mcls, name, bases, ns):
annotate = annotationlib.get_annotate_from_class_namespace(ns)
expected = ns["expected_annotate"]
with self.subTest(name=name):
if expected:
self.assertIsNotNone(annotate)
else:
self.assertIsNone(annotate)
return super().__new__(mcls, name, bases, ns)

class HasAnnotations(metaclass=Meta):
expected_annotate = True
a: int

self.assertIsNone(get_annotate_function(C))
class NoAnnotations(metaclass=Meta):
expected_annotate = False

D = type("D", (), {})
self.assertIsNone(get_annotate_function(D))
class CustomAnnotate(metaclass=Meta):
expected_annotate = True
def __annotate__(format):
return {}

def test_annotated_class(self):
class C:
a: int
code = """
from __future__ import annotations

self.assertEqual(get_annotate_function(C)(Format.VALUE), {"a": int})
class HasFutureAnnotations(metaclass=Meta):
expected_annotate = False
a: int
"""
exec(textwrap.dedent(code), {"Meta": Meta})


class TestToSource(unittest.TestCase):
Expand Down
4 changes: 2 additions & 2 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2896,7 +2896,7 @@ def __new__(cls, typename, bases, ns):
types = ns["__annotations__"]
field_names = list(types)
annotate = _make_eager_annotate(types)
elif (original_annotate := _lazy_annotationlib.get_annotate_function(ns)) is not None:
elif (original_annotate := _lazy_annotationlib.get_annotate_from_class_namespace(ns)) is not None:
types = _lazy_annotationlib.call_annotate_function(
original_annotate, _lazy_annotationlib.Format.FORWARDREF)
field_names = list(types)
Expand Down Expand Up @@ -3082,7 +3082,7 @@ def __new__(cls, name, bases, ns, total=True):
if "__annotations__" in ns:
own_annotate = None
own_annotations = ns["__annotations__"]
elif (own_annotate := _lazy_annotationlib.get_annotate_function(ns)) is not None:
elif (own_annotate := _lazy_annotationlib.get_annotate_from_class_namespace(ns)) is not None:
own_annotations = _lazy_annotationlib.call_annotate_function(
own_annotate, _lazy_annotationlib.Format.FORWARDREF, owner=tp_dict
)
Expand Down