https://github.com/python/cpython/commit/0cc71cd841d300cf90190a58c72c37e506d2303d
commit: 0cc71cd841d300cf90190a58c72c37e506d2303d
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2026-05-09T14:58:51-07:00
summary:

[3.14] gh-141388: Improve docs/tests for non-function callables as annotate 
functions (GH-142327) (#149304)

gh-141388: Improve docs/tests for non-function callables as annotate functions 
(GH-142327)
(cherry picked from commit c1940bcfc8a80d0d66f3f7f03e776d0d23ebb59b)

Co-authored-by: dr-carlos <[email protected]>

files:
A Misc/NEWS.d/next/Library/2025-12-06-08-48-26.gh-issue-141449.hQvNW_.rst
M Doc/glossary.rst
M Doc/library/annotationlib.rst
M Lib/test/test_annotationlib.py

diff --git a/Doc/glossary.rst b/Doc/glossary.rst
index 6151143a97b420..56bc799d945e7b 100644
--- a/Doc/glossary.rst
+++ b/Doc/glossary.rst
@@ -39,10 +39,11 @@ Glossary
       ABCs with the :mod:`abc` module.
 
    annotate function
-      A function that can be called to retrieve the :term:`annotations 
<annotation>`
-      of an object. This function is accessible as the 
:attr:`~object.__annotate__`
-      attribute of functions, classes, and modules. Annotate functions are a
-      subset of :term:`evaluate functions <evaluate function>`.
+      A callable that can be called to retrieve the :term:`annotations 
<annotation>` of
+      an object. Annotate functions are usually :term:`functions <function>`,
+      automatically generated as the :attr:`~object.__annotate__` attribute of 
functions,
+      classes, and modules. Annotate functions are a subset of
+      :term:`evaluate functions <evaluate function>`.
 
    annotation
       A label associated with a variable, a class
diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst
index 40f2a6dc30460b..af28fe0e2fde2f 100644
--- a/Doc/library/annotationlib.rst
+++ b/Doc/library/annotationlib.rst
@@ -510,6 +510,81 @@ annotations from the class and puts them in a separate 
attribute:
          return typ
 
 
+Creating a custom callable annotate function
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Custom :term:`annotate functions <annotate function>` may be literal functions 
like those
+automatically generated for functions, classes, and modules. Or, they may wish 
to utilise
+the encapsulation provided by classes, in which case any :term:`callable` can 
be used as
+an :term:`annotate function`.
+
+To provide the :attr:`~Format.VALUE`, :attr:`~Format.STRING`, or
+:attr:`~Format.FORWARDREF` formats directly, an :term:`annotate function` must 
provide
+the following attribute:
+
+* A callable ``__call__`` with signature ``__call__(format, /) -> dict``, that 
does not
+  raise a :exc:`NotImplementedError` when called with a supported format.
+
+To provide the :attr:`~Format.VALUE_WITH_FAKE_GLOBALS` format, which is used to
+automatically generate :attr:`~Format.STRING` or :attr:`~Format.FORWARDREF` if 
they are
+not supported directly, :term:`annotate functions <annotate function>` must 
provide the
+following attributes:
+
+* A callable ``__call__`` with signature ``__call__(format, /) -> dict``, that 
does not
+  raise a :exc:`NotImplementedError` when called with
+  :attr:`~Format.VALUE_WITH_FAKE_GLOBALS`.
+* A :ref:`code object <code-objects>` ``__code__`` containing the compiled 
code for the
+  annotate function.
+* Optional: A tuple of the function's positional defaults ``__kwdefaults__``, 
if the
+  function represented by ``__code__`` uses any positional defaults.
+* Optional: A dict of the function's keyword defaults ``__defaults__``, if the 
function
+  represented by ``__code__`` uses any keyword defaults.
+* Optional: All other :ref:`function attributes <inspect-types>`.
+
+.. code-block:: python
+
+   class Annotate:
+       called_formats = []
+
+       def __call__(self, format=None, /, *, _self=None):
+           # When called with fake globals, `_self` will be the
+           # actual self value, and `self` will be the format.
+           if _self is not None:
+               self, format = _self, self
+
+           self.called_formats.append(format)
+           if format <= 2:  # VALUE or VALUE_WITH_FAKE_GLOBALS
+               return {"x": MyType}
+           raise NotImplementedError
+
+       __code__ = __call__.__code__
+       __defaults__ = (None,)
+       __kwdefaults__ = property(lambda self: dict(_self=self))
+
+       __globals__ = {}
+       __builtins__ = {}
+       __closure__ = None
+
+This can then be called with:
+
+.. code-block:: pycon
+
+   >>> from annotationlib import call_annotate_function, Format
+   >>> call_annotate_function(Annotate(), format=Format.STRING)
+   {'x': 'MyType'}
+
+Or used as the annotate function for an object:
+
+.. code-block:: pycon
+
+   >>> from annotationlib import get_annotations, Format
+   >>> class C:
+   ...   pass
+   >>> C.__annotate__ = Annotate()
+   >>> get_annotations(Annotate(), format=Format.STRING)
+   {'x': 'MyType'}
+
+
 Limitations of the ``STRING`` format
 ------------------------------------
 
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index 77f2a77882fce2..5087c3ca425f1f 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -1619,6 +1619,84 @@ def annotate(format, /):
             # Some non-Format value
             annotationlib.call_annotate_function(annotate, 7)
 
+    def test_basic_non_function_annotate(self):
+        class Annotate:
+            def __call__(self, format, /, __Format=Format,
+                         __NotImplementedError=NotImplementedError):
+                if format == __Format.VALUE:
+                    return {'x': str}
+                elif format == __Format.VALUE_WITH_FAKE_GLOBALS:
+                    return {'x': int}
+                elif format == __Format.STRING:
+                    return {'x': "float"}
+                else:
+                    raise __NotImplementedError(format)
+
+        annotations = annotationlib.call_annotate_function(Annotate(), 
Format.VALUE)
+        self.assertEqual(annotations, {"x": str})
+
+        annotations = annotationlib.call_annotate_function(Annotate(), 
Format.STRING)
+        self.assertEqual(annotations, {"x": "float"})
+
+        with self.assertRaises(AttributeError) as cm:
+            annotations = annotationlib.call_annotate_function(
+                Annotate(), Format.FORWARDREF
+            )
+
+        self.assertEqual(cm.exception.name, "__builtins__")
+        self.assertIsInstance(cm.exception.obj, Annotate)
+
+    def test_full_non_function_annotate(self):
+        def outer():
+            local = str
+
+            class Annotate:
+                called_formats = []
+
+                def __call__(self, format=None, *, _self=None):
+                    nonlocal local
+                    if _self is not None:
+                        self, format = _self, self
+
+                    self.called_formats.append(format)
+                    if format == 1:  # VALUE
+                        return {"x": MyClass, "y": int, "z": local}
+                    if format == 2:  # VALUE_WITH_FAKE_GLOBALS
+                        return {"w": unknown, "x": MyClass, "y": int, "z": 
local}
+                    raise NotImplementedError
+
+                __globals__ = {"MyClass": MyClass}
+                __builtins__ = {"int": int}
+                __closure__ = (types.CellType(str),)
+                __defaults__ = (None,)
+
+                __kwdefaults__ = property(lambda self: dict(_self=self))
+                __code__ = property(lambda self: self.__call__.__code__)
+
+            return Annotate()
+
+        annotate = outer()
+
+        self.assertEqual(
+            annotationlib.call_annotate_function(annotate, Format.VALUE),
+            {"x": MyClass, "y": int, "z": str}
+        )
+        self.assertEqual(annotate.called_formats[-1], Format.VALUE)
+
+        self.assertEqual(
+            annotationlib.call_annotate_function(annotate, Format.STRING),
+            {"w": "unknown", "x": "MyClass", "y": "int", "z": "local"}
+        )
+        self.assertIn(Format.STRING, annotate.called_formats)
+        self.assertEqual(annotate.called_formats[-1], 
Format.VALUE_WITH_FAKE_GLOBALS)
+
+        self.assertEqual(
+            annotationlib.call_annotate_function(annotate, Format.FORWARDREF),
+            {"w": support.EqualToForwardRef("unknown"), "x": MyClass, "y": 
int, "z": str}
+        )
+        self.assertIn(Format.FORWARDREF, annotate.called_formats)
+        self.assertEqual(annotate.called_formats[-1], 
Format.VALUE_WITH_FAKE_GLOBALS)
+
     def test_error_from_value_raised(self):
         # Test that the error from format.VALUE is raised
         # if all formats fail
diff --git 
a/Misc/NEWS.d/next/Library/2025-12-06-08-48-26.gh-issue-141449.hQvNW_.rst 
b/Misc/NEWS.d/next/Library/2025-12-06-08-48-26.gh-issue-141449.hQvNW_.rst
new file mode 100644
index 00000000000000..4e94c3c80d780f
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-12-06-08-48-26.gh-issue-141449.hQvNW_.rst
@@ -0,0 +1,2 @@
+Improve tests and documentation for non-function callables as
+:term:`annotate functions <annotate function>`.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to