Skip to content

Commit c1940bc

Browse files
authored
gh-141388: Improve docs/tests for non-function callables as annotate functions (#142327)
1 parent 6b632ce commit c1940bc

4 files changed

Lines changed: 160 additions & 4 deletions

File tree

Doc/glossary.rst

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,11 @@ Glossary
3939
ABCs with the :mod:`abc` module.
4040

4141
annotate function
42-
A function that can be called to retrieve the :term:`annotations <annotation>`
43-
of an object. This function is accessible as the :attr:`~object.__annotate__`
44-
attribute of functions, classes, and modules. Annotate functions are a
45-
subset of :term:`evaluate functions <evaluate function>`.
42+
A callable that can be called to retrieve the :term:`annotations <annotation>` of
43+
an object. Annotate functions are usually :term:`functions <function>`,
44+
automatically generated as the :attr:`~object.__annotate__` attribute of functions,
45+
classes, and modules. Annotate functions are a subset of
46+
:term:`evaluate functions <evaluate function>`.
4647

4748
annotation
4849
A label associated with a variable, a class

Doc/library/annotationlib.rst

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,81 @@ annotations from the class and puts them in a separate attribute:
510510
return typ
511511
512512
513+
Creating a custom callable annotate function
514+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
515+
516+
Custom :term:`annotate functions <annotate function>` may be literal functions like those
517+
automatically generated for functions, classes, and modules. Or, they may wish to utilise
518+
the encapsulation provided by classes, in which case any :term:`callable` can be used as
519+
an :term:`annotate function`.
520+
521+
To provide the :attr:`~Format.VALUE`, :attr:`~Format.STRING`, or
522+
:attr:`~Format.FORWARDREF` formats directly, an :term:`annotate function` must provide
523+
the following attribute:
524+
525+
* A callable ``__call__`` with signature ``__call__(format, /) -> dict``, that does not
526+
raise a :exc:`NotImplementedError` when called with a supported format.
527+
528+
To provide the :attr:`~Format.VALUE_WITH_FAKE_GLOBALS` format, which is used to
529+
automatically generate :attr:`~Format.STRING` or :attr:`~Format.FORWARDREF` if they are
530+
not supported directly, :term:`annotate functions <annotate function>` must provide the
531+
following attributes:
532+
533+
* A callable ``__call__`` with signature ``__call__(format, /) -> dict``, that does not
534+
raise a :exc:`NotImplementedError` when called with
535+
:attr:`~Format.VALUE_WITH_FAKE_GLOBALS`.
536+
* A :ref:`code object <code-objects>` ``__code__`` containing the compiled code for the
537+
annotate function.
538+
* Optional: A tuple of the function's positional defaults ``__kwdefaults__``, if the
539+
function represented by ``__code__`` uses any positional defaults.
540+
* Optional: A dict of the function's keyword defaults ``__defaults__``, if the function
541+
represented by ``__code__`` uses any keyword defaults.
542+
* Optional: All other :ref:`function attributes <inspect-types>`.
543+
544+
.. code-block:: python
545+
546+
class Annotate:
547+
called_formats = []
548+
549+
def __call__(self, format=None, /, *, _self=None):
550+
# When called with fake globals, `_self` will be the
551+
# actual self value, and `self` will be the format.
552+
if _self is not None:
553+
self, format = _self, self
554+
555+
self.called_formats.append(format)
556+
if format <= 2: # VALUE or VALUE_WITH_FAKE_GLOBALS
557+
return {"x": MyType}
558+
raise NotImplementedError
559+
560+
__code__ = __call__.__code__
561+
__defaults__ = (None,)
562+
__kwdefaults__ = property(lambda self: dict(_self=self))
563+
564+
__globals__ = {}
565+
__builtins__ = {}
566+
__closure__ = None
567+
568+
This can then be called with:
569+
570+
.. code-block:: pycon
571+
572+
>>> from annotationlib import call_annotate_function, Format
573+
>>> call_annotate_function(Annotate(), format=Format.STRING)
574+
{'x': 'MyType'}
575+
576+
Or used as the annotate function for an object:
577+
578+
.. code-block:: pycon
579+
580+
>>> from annotationlib import get_annotations, Format
581+
>>> class C:
582+
... pass
583+
>>> C.__annotate__ = Annotate()
584+
>>> get_annotations(Annotate(), format=Format.STRING)
585+
{'x': 'MyType'}
586+
587+
513588
Limitations of the ``STRING`` format
514589
------------------------------------
515590

Lib/test/test_annotationlib.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1619,6 +1619,84 @@ def annotate(format, /):
16191619
# Some non-Format value
16201620
annotationlib.call_annotate_function(annotate, 7)
16211621

1622+
def test_basic_non_function_annotate(self):
1623+
class Annotate:
1624+
def __call__(self, format, /, __Format=Format,
1625+
__NotImplementedError=NotImplementedError):
1626+
if format == __Format.VALUE:
1627+
return {'x': str}
1628+
elif format == __Format.VALUE_WITH_FAKE_GLOBALS:
1629+
return {'x': int}
1630+
elif format == __Format.STRING:
1631+
return {'x': "float"}
1632+
else:
1633+
raise __NotImplementedError(format)
1634+
1635+
annotations = annotationlib.call_annotate_function(Annotate(), Format.VALUE)
1636+
self.assertEqual(annotations, {"x": str})
1637+
1638+
annotations = annotationlib.call_annotate_function(Annotate(), Format.STRING)
1639+
self.assertEqual(annotations, {"x": "float"})
1640+
1641+
with self.assertRaises(AttributeError) as cm:
1642+
annotations = annotationlib.call_annotate_function(
1643+
Annotate(), Format.FORWARDREF
1644+
)
1645+
1646+
self.assertEqual(cm.exception.name, "__builtins__")
1647+
self.assertIsInstance(cm.exception.obj, Annotate)
1648+
1649+
def test_full_non_function_annotate(self):
1650+
def outer():
1651+
local = str
1652+
1653+
class Annotate:
1654+
called_formats = []
1655+
1656+
def __call__(self, format=None, *, _self=None):
1657+
nonlocal local
1658+
if _self is not None:
1659+
self, format = _self, self
1660+
1661+
self.called_formats.append(format)
1662+
if format == 1: # VALUE
1663+
return {"x": MyClass, "y": int, "z": local}
1664+
if format == 2: # VALUE_WITH_FAKE_GLOBALS
1665+
return {"w": unknown, "x": MyClass, "y": int, "z": local}
1666+
raise NotImplementedError
1667+
1668+
__globals__ = {"MyClass": MyClass}
1669+
__builtins__ = {"int": int}
1670+
__closure__ = (types.CellType(str),)
1671+
__defaults__ = (None,)
1672+
1673+
__kwdefaults__ = property(lambda self: dict(_self=self))
1674+
__code__ = property(lambda self: self.__call__.__code__)
1675+
1676+
return Annotate()
1677+
1678+
annotate = outer()
1679+
1680+
self.assertEqual(
1681+
annotationlib.call_annotate_function(annotate, Format.VALUE),
1682+
{"x": MyClass, "y": int, "z": str}
1683+
)
1684+
self.assertEqual(annotate.called_formats[-1], Format.VALUE)
1685+
1686+
self.assertEqual(
1687+
annotationlib.call_annotate_function(annotate, Format.STRING),
1688+
{"w": "unknown", "x": "MyClass", "y": "int", "z": "local"}
1689+
)
1690+
self.assertIn(Format.STRING, annotate.called_formats)
1691+
self.assertEqual(annotate.called_formats[-1], Format.VALUE_WITH_FAKE_GLOBALS)
1692+
1693+
self.assertEqual(
1694+
annotationlib.call_annotate_function(annotate, Format.FORWARDREF),
1695+
{"w": support.EqualToForwardRef("unknown"), "x": MyClass, "y": int, "z": str}
1696+
)
1697+
self.assertIn(Format.FORWARDREF, annotate.called_formats)
1698+
self.assertEqual(annotate.called_formats[-1], Format.VALUE_WITH_FAKE_GLOBALS)
1699+
16221700
def test_error_from_value_raised(self):
16231701
# Test that the error from format.VALUE is raised
16241702
# if all formats fail
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Improve tests and documentation for non-function callables as
2+
:term:`annotate functions <annotate function>`.

0 commit comments

Comments
 (0)