]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.14] gh-141388: Improve docs/tests for non-function callables as annotate functions...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Sat, 9 May 2026 21:58:51 +0000 (23:58 +0200)
committerGitHub <noreply@github.com>
Sat, 9 May 2026 21:58:51 +0000 (14:58 -0700)
gh-141388: Improve docs/tests for non-function callables as annotate functions (GH-142327)
(cherry picked from commit c1940bcfc8a80d0d66f3f7f03e776d0d23ebb59b)

Co-authored-by: dr-carlos <77367421+dr-carlos@users.noreply.github.com>
Doc/glossary.rst
Doc/library/annotationlib.rst
Lib/test/test_annotationlib.py
Misc/NEWS.d/next/Library/2025-12-06-08-48-26.gh-issue-141449.hQvNW_.rst [new file with mode: 0644]

index 6151143a97b420d7af567d7ac8fd0751ce0ac28c..56bc799d945e7b0d70e385a4d8e6ffdb318e659d 100644 (file)
@@ -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
index 40f2a6dc30460b7131316c495f77dadbf41a2874..af28fe0e2fde2f83268c27d8a8ac1e3fc1571c7d 100644 (file)
@@ -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
 ------------------------------------
 
index 77f2a77882fce25bad49ba2b2f9a0d49f24e5594..5087c3ca425f1fc8d44c051c9bb52e7a7f955100 100644 (file)
@@ -1619,6 +1619,84 @@ class TestCallAnnotateFunction(unittest.TestCase):
             # 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 (file)
index 0000000..4e94c3c
--- /dev/null
@@ -0,0 +1,2 @@
+Improve tests and documentation for non-function callables as
+:term:`annotate functions <annotate function>`.