mirror of
https://github.com/python/cpython.git
synced 2026-05-04 09:31:02 +00:00
gh-141388: Improve docs/tests for non-function callables as annotate functions (#142327)
This commit is contained in:
parent
6b632ce36b
commit
c1940bcfc8
4 changed files with 160 additions and 4 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
------------------------------------
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
Improve tests and documentation for non-function callables as
|
||||
:term:`annotate functions <annotate function>`.
|
||||
Loading…
Add table
Add a link
Reference in a new issue