From 91036acfe7749e0c242bb1f030700dba2be33a94 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Fri, 5 Dec 2025 16:03:35 +1030 Subject: [PATCH 01/24] Use default values for annotate functions' __globals__, __builtins__, __defaults__, and __kwdefaults__ --- Lib/annotationlib.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index a5788cdbfae3f5..8bc8d064cb262e 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -711,6 +711,9 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): return annotate(format) except NotImplementedError: pass + + annotate_defaults = getattr(annotate, "__defaults__", None) + annotate_kwdefaults = getattr(annotate, "__kwdefaults__", None) if format == Format.STRING: # STRING is implemented by calling the annotate function in a special # environment where every name lookup results in an instance of _Stringifier. @@ -740,8 +743,8 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): annotate.__code__, globals, closure=closure, - argdefs=annotate.__defaults__, - kwdefaults=annotate.__kwdefaults__, + argdefs=annotate_defaults, + kwdefaults=annotate_kwdefaults, ) annos = func(Format.VALUE_WITH_FAKE_GLOBALS) if _is_evaluate: @@ -768,11 +771,12 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): # reconstruct the source. But in the dictionary that we eventually return, we # want to return objects with more user-friendly behavior, such as an __eq__ # that returns a bool and an defined set of attributes. - namespace = {**annotate.__builtins__, **annotate.__globals__} + annotate_globals = getattr(annotate, "__globals__", {}) + namespace = {**getattr(annotate, "__builtins__", builtins.__dict__), **annotate_globals} is_class = isinstance(owner, type) globals = _StringifierDict( namespace, - globals=annotate.__globals__, + globals=annotate_globals, owner=owner, is_class=is_class, format=format, @@ -784,8 +788,8 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): annotate.__code__, globals, closure=closure, - argdefs=annotate.__defaults__, - kwdefaults=annotate.__kwdefaults__, + argdefs=annotate_defaults, + kwdefaults=annotate_kwdefaults, ) try: result = func(Format.VALUE_WITH_FAKE_GLOBALS) @@ -802,7 +806,7 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): # a value in certain cases where an exception gets raised during evaluation. globals = _StringifierDict( {}, - globals=annotate.__globals__, + globals=annotate_globals, owner=owner, is_class=is_class, format=format, @@ -814,8 +818,8 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): annotate.__code__, globals, closure=closure, - argdefs=annotate.__defaults__, - kwdefaults=annotate.__kwdefaults__, + argdefs=annotate_defaults, + kwdefaults=annotate_kwdefaults, ) result = func(Format.VALUE_WITH_FAKE_GLOBALS) globals.transmogrify(cell_dict) From 414251ba6df68e4d974e3c923011e746472213c8 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Fri, 5 Dec 2025 16:05:19 +1030 Subject: [PATCH 02/24] Improve error messages for annotate functions missing __code__ attribute --- Lib/annotationlib.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 8bc8d064cb262e..5e11cebbcd06f1 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -739,8 +739,16 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): closure, _ = _build_closure( annotate, owner, is_class, globals, allow_evaluation=False ) + try: + annotate_code = annotate.__code__ + except AttributeError: + raise AttributeError( + "annotate function requires __code__ attribute", + name="__code__", + obj=annotate + ) func = types.FunctionType( - annotate.__code__, + annotate_code, globals, closure=closure, argdefs=annotate_defaults, @@ -784,8 +792,16 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): closure, cell_dict = _build_closure( annotate, owner, is_class, globals, allow_evaluation=True ) + try: + annotate_code = annotate.__code__ + except AttributeError: + raise AttributeError( + "annotate function requires __code__ attribute", + name="__code__", + obj=annotate + ) func = types.FunctionType( - annotate.__code__, + annotate_code, globals, closure=closure, argdefs=annotate_defaults, @@ -815,7 +831,7 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): annotate, owner, is_class, globals, allow_evaluation=False ) func = types.FunctionType( - annotate.__code__, + annotate_code, globals, closure=closure, argdefs=annotate_defaults, From fd6125d5be3fe82efaa1cc581e7821a25d0d2a25 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Fri, 5 Dec 2025 16:40:09 +1030 Subject: [PATCH 03/24] Add non-function annotate tests --- Lib/test/test_annotationlib.py | 136 +++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 8208d0e9c94819..0640c1840bf567 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -8,6 +8,7 @@ import itertools import pickle from string.templatelib import Template, Interpolation +import types import typing import sys import unittest @@ -1590,6 +1591,141 @@ 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.assertRaisesRegex( + AttributeError, + "annotate function requires __code__ attribute" + ): + annotations = annotationlib.call_annotate_function( + Annotate(), Format.FORWARDREF + ) + + def test_non_function_annotate(self): + class Annotate: + called_formats = [] + + def __call__(self, format=None, *, _self=None): + 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 + + @property + def __defaults__(self): + return (None,) + + @property + def __kwdefaults__(self): + return {"_self": self} + + @property + def __code__(self): + return self.__call__.__code__ + + annotate = Annotate() + + with self.assertRaises(NameError): + annotationlib.call_annotate_function(annotate, Format.VALUE) + self.assertEqual(annotate.called_formats[-1], Format.VALUE) + + annotations = annotationlib.call_annotate_function(annotate, Format.STRING) + self.assertEqual(annotations["x"], "MyType") + self.assertIn(Format.STRING, annotate.called_formats) + self.assertEqual(annotate.called_formats[-1], Format.VALUE_WITH_FAKE_GLOBALS) + + annotations = annotationlib.call_annotate_function(annotate, Format.FORWARDREF) + self.assertEqual(annotations["x"], support.EqualToForwardRef("MyType")) + self.assertIn(Format.FORWARDREF, annotate.called_formats) + self.assertEqual(annotate.called_formats[-1], Format.VALUE_WITH_FAKE_GLOBALS) + + 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 + + @property + def __globals__(self): + return {"MyClass": MyClass} + + @property + def __builtins__(self): + return {"int": int} + + @property + def __closure__(self): + return (types.CellType(str),) + + @property + def __defaults__(self): + return (None,) + + @property + def __kwdefaults__(self): + return {"_self": self} + + @property + def __code__(self): + return 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 From c008676dbf68be61a7a2ae6036f63545eece9808 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Fri, 5 Dec 2025 16:40:42 +1030 Subject: [PATCH 04/24] Don't require __closure__ and __globals__ on annotate functions --- Lib/annotationlib.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 5e11cebbcd06f1..9b2665d3ab6c87 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -737,7 +737,8 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): globals = _StringifierDict({}, format=format) is_class = isinstance(owner, type) closure, _ = _build_closure( - annotate, owner, is_class, globals, allow_evaluation=False + annotate, owner, is_class, globals, + getattr(annotate, "__globals__", {}), allow_evaluation=False ) try: annotate_code = annotate.__code__ @@ -790,7 +791,7 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): format=format, ) closure, cell_dict = _build_closure( - annotate, owner, is_class, globals, allow_evaluation=True + annotate, owner, is_class, globals, annotate_globals, allow_evaluation=True ) try: annotate_code = annotate.__code__ @@ -828,7 +829,7 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): format=format, ) closure, cell_dict = _build_closure( - annotate, owner, is_class, globals, allow_evaluation=False + annotate, owner, is_class, globals, annotate_globals, allow_evaluation=False ) func = types.FunctionType( annotate_code, @@ -861,12 +862,12 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): raise ValueError(f"Invalid format: {format!r}") -def _build_closure(annotate, owner, is_class, stringifier_dict, *, allow_evaluation): - if not annotate.__closure__: +def _build_closure(annotate, owner, is_class, stringifier_dict, annotate_globals, *, allow_evaluation): + if not (annotate_closure := getattr(annotate, "__closure__", None)): return None, None new_closure = [] cell_dict = {} - for name, cell in zip(annotate.__code__.co_freevars, annotate.__closure__, strict=True): + for name, cell in zip(annotate.__code__.co_freevars, annotate_closure, strict=True): cell_dict[name] = cell new_cell = None if allow_evaluation: @@ -881,7 +882,7 @@ def _build_closure(annotate, owner, is_class, stringifier_dict, *, allow_evaluat name, cell=cell, owner=owner, - globals=annotate.__globals__, + globals=annotate_globals, is_class=is_class, stringifier_dict=stringifier_dict, ) From 1ab0139370d4bc60171f2454da29131be78864a8 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Fri, 5 Dec 2025 21:44:48 +1030 Subject: [PATCH 05/24] Clarify type of annotate function in glossary --- Doc/glossary.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/glossary.rst b/Doc/glossary.rst index a4066d42927f64..ff66d16676d9e5 100644 --- a/Doc/glossary.rst +++ b/Doc/glossary.rst @@ -39,10 +39,10 @@ Glossary ABCs with the :mod:`abc` module. annotate function - A function that can be called to retrieve the :term:`annotations ` - 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 `. + A callable, usually a function, that can be called to retrieve the + :term:`annotations ` of an object. An annotate function is accessible + as the :attr:`~object.__annotate__` attribute of functions, classes, and modules. + Annotate functions are a subset of :term:`evaluate functions `. annotation A label associated with a variable, a class From e17762101ba59295de65d612272dbacce2bcb763 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Fri, 5 Dec 2025 22:20:08 +1030 Subject: [PATCH 06/24] Improve backup paths for annotate functions without __builtins__ --- Lib/annotationlib.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 9b2665d3ab6c87..09689f914410ab 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -781,7 +781,12 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): # want to return objects with more user-friendly behavior, such as an __eq__ # that returns a bool and an defined set of attributes. annotate_globals = getattr(annotate, "__globals__", {}) - namespace = {**getattr(annotate, "__builtins__", builtins.__dict__), **annotate_globals} + if annotate_builtins := getattr(annotate, "__builtins__", None): + namespace = {**annotate_builtins, **annotate_globals} + elif annotate_builtins := annotate_globals.get("__builtins__"): + namespace = {**annotate_builtins, **annotate_globals} + else: + namespace = {**builtins.__dict__, **annotate_globals} is_class = isinstance(owner, type) globals = _StringifierDict( namespace, From fe84920048028df246d959d1efc34786b1af4744 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Fri, 5 Dec 2025 23:08:56 +1030 Subject: [PATCH 07/24] Add recipe to docs clarifying how non-function annotate functions work --- Doc/library/annotationlib.rst | 66 +++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index 40f2a6dc30460b..c17e08b7fc90d7 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -510,6 +510,72 @@ annotations from the class and puts them in a separate attribute: return typ + +Creating a custom callable annotate function +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Custom :term:`annotate functions ` 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`. + +However, :term:`methods `, class instances that implement +:meth:`object.__call__`, and most other callables, do not provide the same attributes as +true functions, which are needed for the :attr:`~Format.VALUE_WITH_FAKE_GLOBALS` +machinery to work. :func:`call_annotate_function` and other :mod:`annotationlib` +functions will attempt to infer those attributes where possible, but some of them must +always be present for :attr:`~Format.VALUE_WITH_FAKE_GLOBALS` to work. + +Below is an example of a callable class that provides the necessary attributes to be +used with all formats, and takes advantage of class encapsulation: + +.. 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 + + @property + def __defaults__(self): + return (None,) + + @property + def __kwdefaults__(self): + return {"_self": self} + + @property + def __code__(self): + return self.__call__.__code__ + +This can then be called with: + +.. doctest:: + + >>> 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: + +.. doctest:: + + >>> 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 ------------------------------------ From d42d8ad7a888fd74a8771b5fc81e55da899d51b8 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sat, 6 Dec 2025 08:55:59 +1030 Subject: [PATCH 08/24] Add NEWS entry --- .../next/Library/2025-12-06-08-48-26.gh-issue-141449.hQvNW_.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-12-06-08-48-26.gh-issue-141449.hQvNW_.rst 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..8270132d34d064 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-12-06-08-48-26.gh-issue-141449.hQvNW_.rst @@ -0,0 +1,2 @@ +Improve support and clarify documentation for non-function callables as +:term:`annotate functions `. From 45cb956731e7e903cd56fa626df95a56e41c2c0e Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sat, 6 Dec 2025 09:00:46 +1030 Subject: [PATCH 09/24] Clarify wording in annotate function documentation --- Doc/glossary.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Doc/glossary.rst b/Doc/glossary.rst index ff66d16676d9e5..11fbe06412a566 100644 --- a/Doc/glossary.rst +++ b/Doc/glossary.rst @@ -39,10 +39,11 @@ Glossary ABCs with the :mod:`abc` module. annotate function - A callable, usually a function, that can be called to retrieve the - :term:`annotations ` of an object. An annotate function is accessible - as the :attr:`~object.__annotate__` attribute of functions, classes, and modules. - Annotate functions are a subset of :term:`evaluate functions `. + A callable that can be called to retrieve the :term:`annotations ` of + an object. Annotate functions are usually :term:`functions `, + automatically generated as the :attr:`~object.__annotate__` attribute of functions, + classes, and modules. Annotate functions are a subset of + :term:`evaluate functions `. annotation A label associated with a variable, a class From eef70c4e63ef390dcd424ff6e009ff2ea4c7f33f Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sat, 6 Dec 2025 09:03:38 +1030 Subject: [PATCH 10/24] Wrap `_build_closure` definition line to < 90 chars --- Lib/annotationlib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 09689f914410ab..f7592d7f745458 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -867,7 +867,8 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): raise ValueError(f"Invalid format: {format!r}") -def _build_closure(annotate, owner, is_class, stringifier_dict, annotate_globals, *, allow_evaluation): +def _build_closure(annotate, owner, is_class, stringifier_dict, + annotate_globals, *, allow_evaluation): if not (annotate_closure := getattr(annotate, "__closure__", None)): return None, None new_closure = [] From 44f2a45ff5146e2973aad7ccf89e3adb8b8d9c11 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sat, 6 Dec 2025 09:05:15 +1030 Subject: [PATCH 11/24] Clarify NEWS entry --- .../next/Library/2025-12-06-08-48-26.gh-issue-141449.hQvNW_.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 8270132d34d064..8509e98253f57c 100644 --- 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 @@ -1,2 +1,2 @@ -Improve support and clarify documentation for non-function callables as +Improve support, error messages, and documentation for non-function callables as :term:`annotate functions `. From 095cfb5178648b3271eef79cd9f5ca3df759bbe5 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sat, 6 Dec 2025 10:04:28 +1030 Subject: [PATCH 12/24] Change doctest to python codeblock in `Annotate` class recipe --- Doc/library/annotationlib.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index c17e08b7fc90d7..3077f617eaca9f 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -559,7 +559,7 @@ used with all formats, and takes advantage of class encapsulation: This can then be called with: -.. doctest:: +.. code-block:: python >>> from annotationlib import call_annotate_function, Format >>> call_annotate_function(Annotate(), format=Format.STRING) @@ -567,7 +567,7 @@ This can then be called with: Or used as the annotate function for an object: -.. doctest:: +.. code-block:: python >>> from annotationlib import get_annotations, Format >>> class C: From d9bf2e81a14fd1f2705a29d4993d665b4147ea34 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sun, 7 Dec 2025 07:31:03 +1030 Subject: [PATCH 13/24] Remove wrapping of `annotate.__code__` `AttributeError`s --- Lib/annotationlib.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index f7592d7f745458..e91a519cd1c240 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -740,14 +740,7 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): annotate, owner, is_class, globals, getattr(annotate, "__globals__", {}), allow_evaluation=False ) - try: - annotate_code = annotate.__code__ - except AttributeError: - raise AttributeError( - "annotate function requires __code__ attribute", - name="__code__", - obj=annotate - ) + annotate_code = annotate.__code__ func = types.FunctionType( annotate_code, globals, @@ -798,14 +791,7 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): closure, cell_dict = _build_closure( annotate, owner, is_class, globals, annotate_globals, allow_evaluation=True ) - try: - annotate_code = annotate.__code__ - except AttributeError: - raise AttributeError( - "annotate function requires __code__ attribute", - name="__code__", - obj=annotate - ) + annotate_code = annotate.__code__ func = types.FunctionType( annotate_code, globals, From 943181c8b5fddd87e0b320338239ddb6986cb243 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sun, 7 Dec 2025 07:31:34 +1030 Subject: [PATCH 14/24] Wrap line in `annotationlib` --- Lib/annotationlib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index e91a519cd1c240..6f95d120494d1d 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -820,7 +820,8 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): format=format, ) closure, cell_dict = _build_closure( - annotate, owner, is_class, globals, annotate_globals, allow_evaluation=False + annotate, owner, is_class, globals, + annotate_globals, allow_evaluation=False ) func = types.FunctionType( annotate_code, From a1daa6ed8170ef008c282040ace9a2d26a41eac5 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sun, 7 Dec 2025 07:56:15 +1030 Subject: [PATCH 15/24] Improve documentation for custom callable annotate functions --- Doc/library/annotationlib.rst | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index 3077f617eaca9f..bd9245c675e1a1 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -510,7 +510,6 @@ annotations from the class and puts them in a separate attribute: return typ - Creating a custom callable annotate function ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -519,22 +518,33 @@ automatically generated for functions, classes, and modules. Or, they may wish t the encapsulation provided by classes, in which case any :term:`callable` can be used as an :term:`annotate function`. -However, :term:`methods `, class instances that implement -:meth:`object.__call__`, and most other callables, do not provide the same attributes as -true functions, which are needed for the :attr:`~Format.VALUE_WITH_FAKE_GLOBALS` -machinery to work. :func:`call_annotate_function` and other :mod:`annotationlib` -functions will attempt to infer those attributes where possible, but some of them must -always be present for :attr:`~Format.VALUE_WITH_FAKE_GLOBALS` to work. - -Below is an example of a callable class that provides the necessary attributes to be -used with all formats, and takes advantage of class encapsulation: +To provide the :attr:`~Format.VALUE`, :attr:`~Format.STRING`, or +:attr:`~Format.FORWARDREF` formats directly, an :ref:`annotate function` must provide +the following attribute: +- A callable ``__call__`` with signature ``__call__(format, /) -> dict``, that does not +raise a :err:`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 ` must provide the +following attributes: +- A callable ``__call__`` with signature ``__call__(format, /) -> dict``, that does not +raise a :err:`NotImplementedError` when called with +:attr:`~Format.VALUE_WITH_FAKE_GLOBALS`. +- A :ref:`code object ` ``__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 `. .. code-block:: python class Annotate: called_formats = [] - def __call__(self, format=None, *, _self=None): + 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: From 028e0f95343ca8eb0110ff9bd5ee3bfaccff044d Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sun, 7 Dec 2025 07:56:42 +1030 Subject: [PATCH 16/24] Use pycon instead of python in output blocks --- Doc/library/annotationlib.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index bd9245c675e1a1..99f75123a46b57 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -569,7 +569,7 @@ represented by ``__code__`` uses any keyword defaults. This can then be called with: -.. code-block:: python +.. code-block:: pycon >>> from annotationlib import call_annotate_function, Format >>> call_annotate_function(Annotate(), format=Format.STRING) @@ -577,7 +577,7 @@ This can then be called with: Or used as the annotate function for an object: -.. code-block:: python +.. code-block:: pycon >>> from annotationlib import get_annotations, Format >>> class C: From 9cefbaa9e52b40bba69a59f6c72196c3ef5cf88e Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sun, 7 Dec 2025 07:59:34 +1030 Subject: [PATCH 17/24] Actually use dot points where intended in custom callable doc --- Doc/library/annotationlib.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index 99f75123a46b57..b8b3dfd254328d 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -519,25 +519,25 @@ the encapsulation provided by classes, in which case any :term:`callable` can be an :term:`annotate function`. To provide the :attr:`~Format.VALUE`, :attr:`~Format.STRING`, or -:attr:`~Format.FORWARDREF` formats directly, an :ref:`annotate function` must provide +: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 +* A callable ``__call__`` with signature ``__call__(format, /) -> dict``, that does not raise a :err:`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 ` must provide the following attributes: -- A callable ``__call__`` with signature ``__call__(format, /) -> dict``, that does not +* A callable ``__call__`` with signature ``__call__(format, /) -> dict``, that does not raise a :err:`NotImplementedError` when called with :attr:`~Format.VALUE_WITH_FAKE_GLOBALS`. -- A :ref:`code object ` ``__code__`` containing the compiled code for the +* A :ref:`code object ` ``__code__`` containing the compiled code for the annotate function. -- Optional: A tuple of the function's positional defaults ``__kwdefaults__``, if the +* 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 +* 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 `. +* Optional: All other :ref:`function attributes `. .. code-block:: python From 8ec86eef65883a8b555157734664eeb7cd39337c Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sun, 7 Dec 2025 08:05:05 +1030 Subject: [PATCH 18/24] Fix bullet list indentation --- Doc/library/annotationlib.rst | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index b8b3dfd254328d..82bb0cfd3cd4c0 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -521,22 +521,24 @@ 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 :err:`NotImplementedError` when called with a supported format. + 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 ` must provide the following attributes: + * A callable ``__call__`` with signature ``__call__(format, /) -> dict``, that does not -raise a :err:`NotImplementedError` when called with -:attr:`~Format.VALUE_WITH_FAKE_GLOBALS`. + raise a :exc:`NotImplementedError` when called with + :attr:`~Format.VALUE_WITH_FAKE_GLOBALS`. * A :ref:`code object ` ``__code__`` containing the compiled code for the -annotate function. + annotate function. * Optional: A tuple of the function's positional defaults ``__kwdefaults__``, if the -function represented by ``__code__`` uses any positional defaults. + 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. + represented by ``__code__`` uses any keyword defaults. * Optional: All other :ref:`function attributes `. .. code-block:: python From a70a64790c98273a0976debb2df70fa7bb00d66c Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sun, 7 Dec 2025 08:06:21 +1030 Subject: [PATCH 19/24] Start code block at right indentation --- Doc/library/annotationlib.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index 82bb0cfd3cd4c0..c7c917030c984f 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -543,7 +543,7 @@ following attributes: .. code-block:: python - class Annotate: + class Annotate: called_formats = [] def __call__(self, format=None, /, *, _self=None): From 173797366e51d3c9fa79e6c7090276e8b9df48b4 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sun, 7 Dec 2025 12:04:18 +1030 Subject: [PATCH 20/24] Various formatting changes in `library/annotationlib.rst` docs --- Doc/library/annotationlib.rst | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index c7c917030c984f..afb4a7de2f2099 100644 --- a/Doc/library/annotationlib.rst +++ b/Doc/library/annotationlib.rst @@ -544,30 +544,24 @@ following attributes: .. code-block:: python class Annotate: - called_formats = [] + 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 + 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 + self.called_formats.append(format) + if format <= 2: # VALUE or VALUE_WITH_FAKE_GLOBALS + return {"x": MyType} + raise NotImplementedError - @property - def __defaults__(self): - return (None,) + __defaults__ = (None,) - @property - def __kwdefaults__(self): - return {"_self": self} + __kwdefaults__ = property(lambda self: dict(_self=self)) - @property - def __code__(self): - return self.__call__.__code__ + __code__ = property(lambda self: self.__call__.__code__) This can then be called with: @@ -588,6 +582,7 @@ Or used as the annotate function for an object: >>> get_annotations(Annotate(), format=Format.STRING) {'x': 'MyType'} + Limitations of the ``STRING`` format ------------------------------------ From cbc8466f9165bb5151ac936e9e835d7b94a02eab Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sun, 7 Dec 2025 12:12:49 +1030 Subject: [PATCH 21/24] Fix non-function annotate test for non-wrapped AttributeError --- Lib/test/test_annotationlib.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 0640c1840bf567..c027013d02c0e6 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -1610,10 +1610,7 @@ def __call__(self, format, /, __Format=Format, annotations = annotationlib.call_annotate_function(Annotate(), Format.STRING) self.assertEqual(annotations, {"x": "float"}) - with self.assertRaisesRegex( - AttributeError, - "annotate function requires __code__ attribute" - ): + with self.assertRaises(AttributeError) as cm: annotations = annotationlib.call_annotate_function( Annotate(), Format.FORWARDREF ) From 3bfd2e0cd9c4022449104b36cd62253ed5cebcef Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sun, 7 Dec 2025 12:13:30 +1030 Subject: [PATCH 22/24] Remove test_non_function_annotate() and add improve AttributeError test --- Lib/test/test_annotationlib.py | 42 ++-------------------------------- 1 file changed, 2 insertions(+), 40 deletions(-) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index c027013d02c0e6..a817d36b171021 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -1615,46 +1615,8 @@ def __call__(self, format, /, __Format=Format, Annotate(), Format.FORWARDREF ) - def test_non_function_annotate(self): - class Annotate: - called_formats = [] - - def __call__(self, format=None, *, _self=None): - 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 - - @property - def __defaults__(self): - return (None,) - - @property - def __kwdefaults__(self): - return {"_self": self} - - @property - def __code__(self): - return self.__call__.__code__ - - annotate = Annotate() - - with self.assertRaises(NameError): - annotationlib.call_annotate_function(annotate, Format.VALUE) - self.assertEqual(annotate.called_formats[-1], Format.VALUE) - - annotations = annotationlib.call_annotate_function(annotate, Format.STRING) - self.assertEqual(annotations["x"], "MyType") - self.assertIn(Format.STRING, annotate.called_formats) - self.assertEqual(annotate.called_formats[-1], Format.VALUE_WITH_FAKE_GLOBALS) - - annotations = annotationlib.call_annotate_function(annotate, Format.FORWARDREF) - self.assertEqual(annotations["x"], support.EqualToForwardRef("MyType")) - self.assertIn(Format.FORWARDREF, annotate.called_formats) - self.assertEqual(annotate.called_formats[-1], Format.VALUE_WITH_FAKE_GLOBALS) + self.assertEqual(cm.exception.name, "__code__") + self.assertIsInstance(cm.exception.obj, Annotate) def test_full_non_function_annotate(self): def outer(): From 54401281ff654abb7e818ba72d7c9920beccb5ba Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sun, 7 Dec 2025 12:14:00 +1030 Subject: [PATCH 23/24] Simplify/reduce verbosity of class in `test_full_non_function_annotate()` --- Lib/test/test_annotationlib.py | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index a817d36b171021..f66136031f5984 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -1637,29 +1637,13 @@ def __call__(self, format=None, *, _self=None): return {"w": unknown, "x": MyClass, "y": int, "z": local} raise NotImplementedError - @property - def __globals__(self): - return {"MyClass": MyClass} + __globals__ = {"MyClass": MyClass} + __builtins__ = {"int": int} + __closure__ = (types.CellType(str),) + __defaults__ = (None,) - @property - def __builtins__(self): - return {"int": int} - - @property - def __closure__(self): - return (types.CellType(str),) - - @property - def __defaults__(self): - return (None,) - - @property - def __kwdefaults__(self): - return {"_self": self} - - @property - def __code__(self): - return self.__call__.__code__ + __kwdefaults__ = property(lambda self: dict(_self=self)) + __code__ = property(lambda self: self.__call__.__code__) return Annotate() From 46ef8b102ba4d53b76fba5acbdc893f781c0ddfa Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sun, 7 Dec 2025 12:16:23 +1030 Subject: [PATCH 24/24] Remove defaults fro non-function annotates in `call_annotate_function()` --- Lib/annotationlib.py | 52 +++++++++++++--------------------- Lib/test/test_annotationlib.py | 2 +- 2 files changed, 20 insertions(+), 34 deletions(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 6f95d120494d1d..a5788cdbfae3f5 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -711,9 +711,6 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): return annotate(format) except NotImplementedError: pass - - annotate_defaults = getattr(annotate, "__defaults__", None) - annotate_kwdefaults = getattr(annotate, "__kwdefaults__", None) if format == Format.STRING: # STRING is implemented by calling the annotate function in a special # environment where every name lookup results in an instance of _Stringifier. @@ -737,16 +734,14 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): globals = _StringifierDict({}, format=format) is_class = isinstance(owner, type) closure, _ = _build_closure( - annotate, owner, is_class, globals, - getattr(annotate, "__globals__", {}), allow_evaluation=False + annotate, owner, is_class, globals, allow_evaluation=False ) - annotate_code = annotate.__code__ func = types.FunctionType( - annotate_code, + annotate.__code__, globals, closure=closure, - argdefs=annotate_defaults, - kwdefaults=annotate_kwdefaults, + argdefs=annotate.__defaults__, + kwdefaults=annotate.__kwdefaults__, ) annos = func(Format.VALUE_WITH_FAKE_GLOBALS) if _is_evaluate: @@ -773,31 +768,24 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): # reconstruct the source. But in the dictionary that we eventually return, we # want to return objects with more user-friendly behavior, such as an __eq__ # that returns a bool and an defined set of attributes. - annotate_globals = getattr(annotate, "__globals__", {}) - if annotate_builtins := getattr(annotate, "__builtins__", None): - namespace = {**annotate_builtins, **annotate_globals} - elif annotate_builtins := annotate_globals.get("__builtins__"): - namespace = {**annotate_builtins, **annotate_globals} - else: - namespace = {**builtins.__dict__, **annotate_globals} + namespace = {**annotate.__builtins__, **annotate.__globals__} is_class = isinstance(owner, type) globals = _StringifierDict( namespace, - globals=annotate_globals, + globals=annotate.__globals__, owner=owner, is_class=is_class, format=format, ) closure, cell_dict = _build_closure( - annotate, owner, is_class, globals, annotate_globals, allow_evaluation=True + annotate, owner, is_class, globals, allow_evaluation=True ) - annotate_code = annotate.__code__ func = types.FunctionType( - annotate_code, + annotate.__code__, globals, closure=closure, - argdefs=annotate_defaults, - kwdefaults=annotate_kwdefaults, + argdefs=annotate.__defaults__, + kwdefaults=annotate.__kwdefaults__, ) try: result = func(Format.VALUE_WITH_FAKE_GLOBALS) @@ -814,21 +802,20 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): # a value in certain cases where an exception gets raised during evaluation. globals = _StringifierDict( {}, - globals=annotate_globals, + globals=annotate.__globals__, owner=owner, is_class=is_class, format=format, ) closure, cell_dict = _build_closure( - annotate, owner, is_class, globals, - annotate_globals, allow_evaluation=False + annotate, owner, is_class, globals, allow_evaluation=False ) func = types.FunctionType( - annotate_code, + annotate.__code__, globals, closure=closure, - argdefs=annotate_defaults, - kwdefaults=annotate_kwdefaults, + argdefs=annotate.__defaults__, + kwdefaults=annotate.__kwdefaults__, ) result = func(Format.VALUE_WITH_FAKE_GLOBALS) globals.transmogrify(cell_dict) @@ -854,13 +841,12 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): raise ValueError(f"Invalid format: {format!r}") -def _build_closure(annotate, owner, is_class, stringifier_dict, - annotate_globals, *, allow_evaluation): - if not (annotate_closure := getattr(annotate, "__closure__", None)): +def _build_closure(annotate, owner, is_class, stringifier_dict, *, allow_evaluation): + if not annotate.__closure__: return None, None new_closure = [] cell_dict = {} - for name, cell in zip(annotate.__code__.co_freevars, annotate_closure, strict=True): + for name, cell in zip(annotate.__code__.co_freevars, annotate.__closure__, strict=True): cell_dict[name] = cell new_cell = None if allow_evaluation: @@ -875,7 +861,7 @@ def _build_closure(annotate, owner, is_class, stringifier_dict, name, cell=cell, owner=owner, - globals=annotate_globals, + globals=annotate.__globals__, is_class=is_class, stringifier_dict=stringifier_dict, ) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index f66136031f5984..1caf16b2602995 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -1615,7 +1615,7 @@ def __call__(self, format, /, __Format=Format, Annotate(), Format.FORWARDREF ) - self.assertEqual(cm.exception.name, "__code__") + self.assertEqual(cm.exception.name, "__builtins__") self.assertIsInstance(cm.exception.obj, Annotate) def test_full_non_function_annotate(self):