Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
75 changes: 75 additions & 0 deletions Doc/library/annotationlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------------------------------

Expand Down
78 changes: 78 additions & 0 deletions Lib/test/test_annotationlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Improve tests and documentation for non-function callables as
:term:`annotate functions <annotate function>`.
Loading