https://github.com/python/cpython/commit/c1940bcfc8a80d0d66f3f7f03e776d0d23ebb59b
commit: c1940bcfc8a80d0d66f3f7f03e776d0d23ebb59b
branch: main
author: dr-carlos <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2026-05-02T18:21:59-07:00
summary:
gh-141388: Improve docs/tests for non-function callables as annotate functions
(#142327)
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]