https://github.com/python/cpython/commit/7cb86c5defa17147c67b56c4227e74e4c5968686
commit: 7cb86c5defa17147c67b56c4227e74e4c5968686
branch: main
author: Jelle Zijlstra <jelle.zijls...@gmail.com>
committer: JelleZijlstra <jelle.zijls...@gmail.com>
date: 2025-05-04T07:26:42-07:00
summary:

gh-132426: Add get_annotate_from_class_namespace replacing 
get_annotate_function (#132490)

As noted on the issue, making get_annotate_function() support both types and
mappings is problematic because one object may be both. So let's add a new one
that works with any mapping.

This leaves get_annotate_function() not very useful, so remove it.

files:
A Misc/NEWS.d/next/Library/2025-04-13-21-11-11.gh-issue-132426.SZno1d.rst
M Doc/library/annotationlib.rst
M Doc/reference/datamodel.rst
M Lib/annotationlib.py
M Lib/test/test_annotationlib.py
M Lib/typing.py

diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst
index 7946cd3a3ced34..b9932a9e4cca1f 100644
--- a/Doc/library/annotationlib.rst
+++ b/Doc/library/annotationlib.rst
@@ -40,7 +40,7 @@ The :func:`get_annotations` function is the main entry point 
for
 retrieving annotations. Given a function, class, or module, it returns
 an annotations dictionary in the requested format. This module also provides
 functionality for working directly with the :term:`annotate function`
-that is used to evaluate annotations, such as :func:`get_annotate_function`
+that is used to evaluate annotations, such as 
:func:`get_annotate_from_class_namespace`
 and :func:`call_annotate_function`, as well as the
 :func:`call_evaluate_function` function for working with
 :term:`evaluate functions <evaluate function>`.
@@ -300,15 +300,13 @@ Functions
 
    .. versionadded:: 3.14
 
-.. function:: get_annotate_function(obj)
+.. function:: get_annotate_from_class_namespace(namespace)
 
-   Retrieve the :term:`annotate function` for *obj*. Return :const:`!None`
-   if *obj* does not have an annotate function. *obj* may be a class, function,
-   module, or a namespace dictionary for a class. The last case is useful 
during
-   class creation, e.g. in the ``__new__`` method of a metaclass.
-
-   This is usually equivalent to accessing the :attr:`~object.__annotate__`
-   attribute of *obj*, but access through this public function is preferred.
+   Retrieve the :term:`annotate function` from a class namespace dictionary 
*namespace*.
+   Return :const:`!None` if the namespace does not contain an annotate 
function.
+   This is primarily useful before the class has been fully created (e.g., in 
a metaclass);
+   after the class exists, the annotate function can be retrieved with 
``cls.__annotate__``.
+   See :ref:`below <annotationlib-metaclass>` for an example using this 
function in a metaclass.
 
    .. versionadded:: 3.14
 
@@ -407,3 +405,76 @@ Functions
 
    .. versionadded:: 3.14
 
+
+Recipes
+-------
+
+.. _annotationlib-metaclass:
+
+Using annotations in a metaclass
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+A :ref:`metaclass <metaclasses>` may want to inspect or even modify the 
annotations
+in a class body during class creation. Doing so requires retrieving annotations
+from the class namespace dictionary. For classes created with
+``from __future__ import annotations``, the annotations will be in the 
``__annotations__``
+key of the dictionary. For other classes with annotations,
+:func:`get_annotate_from_class_namespace` can be used to get the
+annotate function, and :func:`call_annotate_function` can be used to call it 
and
+retrieve the annotations. Using the :attr:`~Format.FORWARDREF` format will 
usually
+be best, because this allows the annotations to refer to names that cannot yet 
be
+resolved when the class is created.
+
+To modify the annotations, it is best to create a wrapper annotate function
+that calls the original annotate function, makes any necessary adjustments, and
+returns the result.
+
+Below is an example of a metaclass that filters out all 
:class:`typing.ClassVar`
+annotations from the class and puts them in a separate attribute:
+
+.. code-block:: python
+
+   import annotationlib
+   import typing
+
+   class ClassVarSeparator(type):
+      def __new__(mcls, name, bases, ns):
+         if "__annotations__" in ns:  # from __future__ import annotations
+            annotations = ns["__annotations__"]
+            classvar_keys = {
+               key for key, value in annotations.items()
+               # Use string comparison for simplicity; a more robust solution
+               # could use annotationlib.ForwardRef.evaluate
+               if value.startswith("ClassVar")
+            }
+            classvars = {key: annotations[key] for key in classvar_keys}
+            ns["__annotations__"] = {
+               key: value for key, value in annotations.items()
+               if key not in classvar_keys
+            }
+            wrapped_annotate = None
+         elif annotate := annotationlib.get_annotate_from_class_namespace(ns):
+            annotations = annotationlib.call_annotate_function(
+               annotate, format=annotationlib.Format.FORWARDREF
+            )
+            classvar_keys = {
+               key for key, value in annotations.items()
+               if typing.get_origin(value) is typing.ClassVar
+            }
+            classvars = {key: annotations[key] for key in classvar_keys}
+
+            def wrapped_annotate(format):
+               annos = annotationlib.call_annotate_function(annotate, format, 
owner=typ)
+               return {key: value for key, value in annos.items() if key not 
in classvar_keys}
+
+         else:  # no annotations
+            classvars = {}
+            wrapped_annotate = None
+         typ = super().__new__(mcls, name, bases, ns)
+
+         if wrapped_annotate is not None:
+            # Wrap the original __annotate__ with a wrapper that removes 
ClassVars
+            typ.__annotate__ = wrapped_annotate
+         typ.classvars = classvars  # Store the ClassVars in a separate 
attribute
+         return typ
+
diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst
index b3096a9f0c3820..ea5b84b00c008c 100644
--- a/Doc/reference/datamodel.rst
+++ b/Doc/reference/datamodel.rst
@@ -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>`.
@@ -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__
diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py
index 971f636f9714d7..37f51e69f94127 100644
--- a/Lib/annotationlib.py
+++ b/Lib/annotationlib.py
@@ -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",
@@ -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(
@@ -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):
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index be55f044b15ccb..404a8ccc9d3741 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -1,5 +1,6 @@
 """Tests for the annotations module."""
 
+import textwrap
 import annotationlib
 import builtins
 import collections
@@ -12,7 +13,6 @@
     Format,
     ForwardRef,
     get_annotations,
-    get_annotate_function,
     annotations_to_string,
     type_repr,
 )
@@ -933,13 +933,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):
@@ -952,13 +952,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
@@ -996,7 +996,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
@@ -1005,25 +1005,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 TestTypeRepr(unittest.TestCase):
diff --git a/Lib/typing.py b/Lib/typing.py
index f70dcd0b5b7b5c..e019c5975800a9 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -2906,7 +2906,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)
@@ -3092,7 +3092,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
             )
diff --git 
a/Misc/NEWS.d/next/Library/2025-04-13-21-11-11.gh-issue-132426.SZno1d.rst 
b/Misc/NEWS.d/next/Library/2025-04-13-21-11-11.gh-issue-132426.SZno1d.rst
new file mode 100644
index 00000000000000..1f2b9a2df936c6
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-04-13-21-11-11.gh-issue-132426.SZno1d.rst
@@ -0,0 +1,3 @@
+Add :func:`annotationlib.get_annotate_from_class_namespace` as a helper for
+accessing annotations in metaclasses, and remove
+``annotationlib.get_annotate_function``.

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to