From ed99f4ca2949168243b55380443f1e5b5cae3244 Mon Sep 17 00:00:00 2001 From: David C Ellis Date: Wed, 10 Sep 2025 19:45:48 +0100 Subject: [PATCH 1/5] Add tests for calling a custom annotate function that only implements VALUE annotations --- Lib/test/test_annotationlib.py | 69 ++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 88e0d611647f28..40cdff377b185c 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -10,6 +10,7 @@ from string.templatelib import Template import typing import unittest +import unittest.mock from annotationlib import ( Format, ForwardRef, @@ -1206,6 +1207,74 @@ def evaluate(format, exc=NotImplementedError): ) +class TestCallAnnotateFunction(unittest.TestCase): + def _annotate_mock(self): + def annotate(format, /): + if format == Format.VALUE: + return {"x": str} + else: + raise NotImplementedError(format) + + annotate_mock = unittest.mock.MagicMock( + wraps=annotate + ) + + # Add missing magic attributes needed + required_magic = [ + "__builtins__", + "__closure__", + "__code__", + "__defaults__", + "__globals__", + "__kwdefaults__", + ] + + for attrib in required_magic: + setattr(annotate_mock, attrib, getattr(annotate, attrib)) + + return annotate_mock + + def test_user_annotate_value(self): + annotate = self._annotate_mock() + + annotations = annotationlib.call_annotate_function( + annotate, + Format.VALUE, + ) + + self.assertEqual(annotations, {"x": str}) + annotate.assert_called_once_with(Format.VALUE) + + def test_user_annotate_forwardref(self): + annotate = self._annotate_mock() + + with self.assertRaises(NotImplementedError): + annotations = annotationlib.call_annotate_function( + annotate, + Format.FORWARDREF, + ) + + annotate.assert_has_calls([ + unittest.mock.Call(Format.FORWARDREF), + unittest.mock.Call(Format.VALUE_WITH_FAKE_GLOBALS), + ]) + + def test_user_annotate_string(self): + annotate = self._annotate_mock() + + with self.assertRaises(NotImplementedError): + annotations = annotationlib.call_annotate_function( + annotate, + Format.STRING, + ) + + annotate.assert_has_calls([ + unittest.mock.Call(Format.STRING), + unittest.mock.Call(Format.VALUE_WITH_FAKE_GLOBALS), + ]) + + + class MetaclassTests(unittest.TestCase): def test_annotated_meta(self): class Meta(type): From 4d88779eaeae2385cd5cf96dc7e7d4cb4477bd5a Mon Sep 17 00:00:00 2001 From: David C Ellis Date: Wed, 10 Sep 2025 19:49:51 +0100 Subject: [PATCH 2/5] call is lower case --- Lib/test/test_annotationlib.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 40cdff377b185c..039c070ed33c0c 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -1255,8 +1255,8 @@ def test_user_annotate_forwardref(self): ) annotate.assert_has_calls([ - unittest.mock.Call(Format.FORWARDREF), - unittest.mock.Call(Format.VALUE_WITH_FAKE_GLOBALS), + unittest.mock.call(Format.FORWARDREF), + unittest.mock.call(Format.VALUE_WITH_FAKE_GLOBALS), ]) def test_user_annotate_string(self): @@ -1269,8 +1269,8 @@ def test_user_annotate_string(self): ) annotate.assert_has_calls([ - unittest.mock.Call(Format.STRING), - unittest.mock.Call(Format.VALUE_WITH_FAKE_GLOBALS), + unittest.mock.call(Format.STRING), + unittest.mock.call(Format.VALUE_WITH_FAKE_GLOBALS), ]) From 8296cd8d833c70e8d437fecc188ad71874c0256b Mon Sep 17 00:00:00 2001 From: David C Ellis Date: Wed, 10 Sep 2025 20:01:13 +0100 Subject: [PATCH 3/5] actual function is only called once --- Lib/test/test_annotationlib.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 039c070ed33c0c..b8355a7cf5e21d 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -1254,10 +1254,9 @@ def test_user_annotate_forwardref(self): Format.FORWARDREF, ) - annotate.assert_has_calls([ - unittest.mock.call(Format.FORWARDREF), - unittest.mock.call(Format.VALUE_WITH_FAKE_GLOBALS), - ]) + # The annotate function itself is not called the second time + # A new function built from the code is called instead + annotate.assert_called_once_with(Format.FORWARDREF) def test_user_annotate_string(self): annotate = self._annotate_mock() @@ -1274,7 +1273,6 @@ def test_user_annotate_string(self): ]) - class MetaclassTests(unittest.TestCase): def test_annotated_meta(self): class Meta(type): From c19bcbdc71b7c3bb71079c3e6433d86cae0f925f Mon Sep 17 00:00:00 2001 From: David C Ellis Date: Wed, 10 Sep 2025 20:04:49 +0100 Subject: [PATCH 4/5] Fail if NotImplementedError is raised for VALUE_WITH_FAKE_GLOBALS --- Lib/annotationlib.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index bee019cd51591e..13590c62bee2d3 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -664,6 +664,17 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): # possibly constants if the annotate function uses them directly). We then # convert each of those into a string to get an approximation of the # original source. + + # Attempt to call with VALUE_WITH_FAKE_GLOBALS to check if it is implemented + # See: https://github.com/python/cpython/issues/138764 + # Only fail on NotImplementedError + try: + annotate(Format.VALUE_WITH_FAKE_GLOBALS) + except NotImplementedError: + raise + except Exception: + pass + globals = _StringifierDict({}, format=format) is_class = isinstance(owner, type) closure = _build_closure( @@ -722,6 +733,10 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False): ) try: result = func(Format.VALUE_WITH_FAKE_GLOBALS) + except NotImplementedError: + # If NotImplementedError is raised, don't try to call again with + # no globals. + raise except Exception: pass else: From 1bc700a070e379df126a941a74ff8c23dae183de Mon Sep 17 00:00:00 2001 From: David C Ellis Date: Thu, 11 Sep 2025 18:30:59 +0100 Subject: [PATCH 5/5] Test the call to the function with fake globals for the FORWARDREF format. --- Lib/test/test_annotationlib.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index b8355a7cf5e21d..5a2d8a558a2891 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 +import types import typing import unittest import unittest.mock @@ -1248,16 +1249,28 @@ def test_user_annotate_value(self): def test_user_annotate_forwardref(self): annotate = self._annotate_mock() - with self.assertRaises(NotImplementedError): - annotations = annotationlib.call_annotate_function( - annotate, - Format.FORWARDREF, - ) + new_annotate = None + functype = types.FunctionType - # The annotate function itself is not called the second time - # A new function built from the code is called instead + def functiontype_mock(*args, **kwargs): + nonlocal new_annotate + new_func = unittest.mock.MagicMock(wraps=functype(*args, **kwargs)) + new_annotate = new_func + return new_func + + with unittest.mock.patch("types.FunctionType", new=functiontype_mock): + with self.assertRaises(NotImplementedError): + annotations = annotationlib.call_annotate_function( + annotate, + Format.FORWARDREF, + ) + + # Test the direct call annotate.assert_called_once_with(Format.FORWARDREF) + # Test the call on the function with fake globals + new_annotate.assert_called_once_with(Format.VALUE_WITH_FAKE_GLOBALS) + def test_user_annotate_string(self): annotate = self._annotate_mock()