Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
91036ac
Use default values for annotate functions' __globals__, __builtins__,…
dr-carlos Dec 5, 2025
414251b
Improve error messages for annotate functions missing __code__ attribute
dr-carlos Dec 5, 2025
fd6125d
Add non-function annotate tests
dr-carlos Dec 5, 2025
c008676
Don't require __closure__ and __globals__ on annotate functions
dr-carlos Dec 5, 2025
1ab0139
Clarify type of annotate function in glossary
dr-carlos Dec 5, 2025
e177621
Improve backup paths for annotate functions without __builtins__
dr-carlos Dec 5, 2025
fe84920
Add recipe to docs clarifying how non-function annotate functions work
dr-carlos Dec 5, 2025
d42d8ad
Add NEWS entry
dr-carlos Dec 5, 2025
45cb956
Clarify wording in annotate function documentation
dr-carlos Dec 5, 2025
eef70c4
Wrap `_build_closure` definition line to < 90 chars
dr-carlos Dec 5, 2025
44f2a45
Clarify NEWS entry
dr-carlos Dec 5, 2025
095cfb5
Change doctest to python codeblock in `Annotate` class recipe
dr-carlos Dec 5, 2025
d9bf2e8
Remove wrapping of `annotate.__code__` `AttributeError`s
dr-carlos Dec 6, 2025
943181c
Wrap line in `annotationlib`
dr-carlos Dec 6, 2025
a1daa6e
Improve documentation for custom callable annotate functions
dr-carlos Dec 6, 2025
028e0f9
Use pycon instead of python in output blocks
dr-carlos Dec 6, 2025
9cefbaa
Actually use dot points where intended in custom callable doc
dr-carlos Dec 6, 2025
8ec86ee
Fix bullet list indentation
dr-carlos Dec 6, 2025
a70a647
Start code block at right indentation
dr-carlos Dec 6, 2025
1737973
Various formatting changes in `library/annotationlib.rst` docs
dr-carlos Dec 7, 2025
cbc8466
Fix non-function annotate test for non-wrapped AttributeError
dr-carlos Dec 7, 2025
3bfd2e0
Remove test_non_function_annotate() and add improve AttributeError test
dr-carlos Dec 7, 2025
5440128
Simplify/reduce verbosity of class in `test_full_non_function_annotat…
dr-carlos Dec 7, 2025
46ef8b1
Remove defaults fro non-function annotates in `call_annotate_function()`
dr-carlos Dec 7, 2025
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
9 changes: 5 additions & 4 deletions Doc/glossary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions Doc/library/annotationlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,79 @@ 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

__defaults__ = (None,)

__kwdefaults__ = property(lambda self: dict(_self=self))

__code__ = property(lambda self: self.__call__.__code__)

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
------------------------------------

Expand Down
79 changes: 79 additions & 0 deletions Lib/test/test_annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import itertools
import pickle
from string.templatelib import Template, Interpolation
import types
import typing
import sys
import unittest
Expand Down Expand Up @@ -1590,6 +1591,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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Improve support, error messages, and documentation for non-function callables as
:term:`annotate functions <annotate function>`.
Loading