https://github.com/python/cpython/commit/95c257e2e691456140e79bd98d1674cbd289eb38
commit: 95c257e2e691456140e79bd98d1674cbd289eb38
branch: main
author: David Ellis <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2025-10-21T15:57:43Z
summary:
gh-138764: annotationlib: Make `call_annotate_function` fallback to using
`VALUE` annotations if both the requested format and `VALUE_WITH_FAKE_GLOBALS`
are not implemented (#138803)
files:
A Misc/NEWS.d/next/Library/2025-09-12-09-34-37.gh-issue-138764.mokHoY.rst
M Doc/library/annotationlib.rst
M Lib/annotationlib.py
M Lib/test/test_annotationlib.py
diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst
index d6f5055955e8cf..40f2a6dc30460b 100644
--- a/Doc/library/annotationlib.rst
+++ b/Doc/library/annotationlib.rst
@@ -340,14 +340,29 @@ Functions
* VALUE: :attr:`!object.__annotations__` is tried first; if that does not
exist,
the :attr:`!object.__annotate__` function is called if it exists.
+
* FORWARDREF: If :attr:`!object.__annotations__` exists and can be
evaluated successfully,
it is used; otherwise, the :attr:`!object.__annotate__` function is
called. If it
does not exist either, :attr:`!object.__annotations__` is tried again and
any error
from accessing it is re-raised.
+
+ * When calling :attr:`!object.__annotate__` it is first called with
:attr:`~Format.FORWARDREF`.
+ If this is not implemented, it will then check if
:attr:`~Format.VALUE_WITH_FAKE_GLOBALS`
+ is supported and use that in the fake globals environment.
+ If neither of these formats are supported, it will fall back to using
:attr:`~Format.VALUE`.
+ If :attr:`~Format.VALUE` fails, the error from this call will be raised.
+
* STRING: If :attr:`!object.__annotate__` exists, it is called first;
otherwise, :attr:`!object.__annotations__` is used and stringified
using :func:`annotations_to_string`.
+ * When calling :attr:`!object.__annotate__` it is first called with
:attr:`~Format.STRING`.
+ If this is not implemented, it will then check if
:attr:`~Format.VALUE_WITH_FAKE_GLOBALS`
+ is supported and use that in the fake globals environment.
+ If neither of these formats are supported, it will fall back to using
:attr:`~Format.VALUE`
+ with the result converted using :func:`annotations_to_string`.
+ If :attr:`~Format.VALUE` fails, the error from this call will be raised.
+
Returns a dict. :func:`!get_annotations` returns a new dict every time
it's called; calling it twice on the same object will return two
different but equivalent dicts.
diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py
index 544e069626d0d9..81886a0467d001 100644
--- a/Lib/annotationlib.py
+++ b/Lib/annotationlib.py
@@ -695,6 +695,18 @@ 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:
+ # Both STRING and VALUE_WITH_FAKE_GLOBALS are not implemented:
fallback to VALUE
+ return annotations_to_string(annotate(Format.VALUE))
+ except Exception:
+ pass
+
globals = _StringifierDict({}, format=format)
is_class = isinstance(owner, type)
closure = _build_closure(
@@ -753,6 +765,9 @@ def call_annotate_function(annotate, format, *, owner=None,
_is_evaluate=False):
)
try:
result = func(Format.VALUE_WITH_FAKE_GLOBALS)
+ except NotImplementedError:
+ # FORWARDREF and VALUE_WITH_FAKE_GLOBALS not supported, fall back
to VALUE
+ return annotate(Format.VALUE)
except Exception:
pass
else:
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index 2c5bf2b3417344..8da4ff096e7593 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -1194,6 +1194,25 @@ class RaisesAttributeError:
},
)
+ def test_raises_error_from_value(self):
+ # test that if VALUE is the only supported format, but raises an error
+ # that error is propagated from get_annotations
+ class DemoException(Exception): ...
+
+ def annotate(format, /):
+ if format == Format.VALUE:
+ raise DemoException()
+ else:
+ raise NotImplementedError(format)
+
+ def f(): ...
+
+ f.__annotate__ = annotate
+
+ for fmt in [Format.VALUE, Format.FORWARDREF, Format.STRING]:
+ with self.assertRaises(DemoException):
+ get_annotations(f, format=fmt)
+
class TestCallEvaluateFunction(unittest.TestCase):
def test_evaluation(self):
@@ -1214,6 +1233,163 @@ def evaluate(format, exc=NotImplementedError):
)
+class TestCallAnnotateFunction(unittest.TestCase):
+ # Tests for user defined annotate functions.
+
+ # Format and NotImplementedError are provided as arguments so they exist in
+ # the fake globals namespace.
+ # This avoids non-matching conditions passing by being converted to
stringifiers.
+ # See: https://github.com/python/cpython/issues/138764
+
+ def test_user_annotate_value(self):
+ def annotate(format, /):
+ if format == Format.VALUE:
+ return {"x": str}
+ else:
+ raise NotImplementedError(format)
+
+ annotations = annotationlib.call_annotate_function(
+ annotate,
+ Format.VALUE,
+ )
+
+ self.assertEqual(annotations, {"x": str})
+
+ def test_user_annotate_forwardref_supported(self):
+ # If Format.FORWARDREF is supported prefer it over Format.VALUE
+ def annotate(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.FORWARDREF:
+ return {'x': float}
+ else:
+ raise __NotImplementedError(format)
+
+ annotations = annotationlib.call_annotate_function(
+ annotate,
+ Format.FORWARDREF
+ )
+
+ self.assertEqual(annotations, {"x": float})
+
+ def test_user_annotate_forwardref_fakeglobals(self):
+ # If Format.FORWARDREF is not supported, use
Format.VALUE_WITH_FAKE_GLOBALS
+ # before falling back to Format.VALUE
+ def annotate(format, /, __Format=Format,
__NotImplementedError=NotImplementedError):
+ if format == __Format.VALUE:
+ return {'x': str}
+ elif format == __Format.VALUE_WITH_FAKE_GLOBALS:
+ return {'x': int}
+ else:
+ raise __NotImplementedError(format)
+
+ annotations = annotationlib.call_annotate_function(
+ annotate,
+ Format.FORWARDREF
+ )
+
+ self.assertEqual(annotations, {"x": int})
+
+ def test_user_annotate_forwardref_value_fallback(self):
+ # If Format.FORWARDREF and Format.VALUE_WITH_FAKE_GLOBALS are not
supported
+ # use Format.VALUE
+ def annotate(format, /, __Format=Format,
__NotImplementedError=NotImplementedError):
+ if format == __Format.VALUE:
+ return {"x": str}
+ else:
+ raise __NotImplementedError(format)
+
+ annotations = annotationlib.call_annotate_function(
+ annotate,
+ Format.FORWARDREF,
+ )
+
+ self.assertEqual(annotations, {"x": str})
+
+ def test_user_annotate_string_supported(self):
+ # If Format.STRING is supported prefer it over Format.VALUE
+ def annotate(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.STRING,
+ )
+
+ self.assertEqual(annotations, {"x": "float"})
+
+ def test_user_annotate_string_fakeglobals(self):
+ # If Format.STRING is not supported but Format.VALUE_WITH_FAKE_GLOBALS
is
+ # prefer that over Format.VALUE
+ def annotate(format, /, __Format=Format,
__NotImplementedError=NotImplementedError):
+ if format == __Format.VALUE:
+ return {'x': str}
+ elif format == __Format.VALUE_WITH_FAKE_GLOBALS:
+ return {'x': int}
+ else:
+ raise __NotImplementedError(format)
+
+ annotations = annotationlib.call_annotate_function(
+ annotate,
+ Format.STRING,
+ )
+
+ self.assertEqual(annotations, {"x": "int"})
+
+ def test_user_annotate_string_value_fallback(self):
+ # If Format.STRING and Format.VALUE_WITH_FAKE_GLOBALS are not
+ # supported fall back to Format.VALUE and convert to strings
+ def annotate(format, /, __Format=Format,
__NotImplementedError=NotImplementedError):
+ if format == __Format.VALUE:
+ return {"x": str}
+ else:
+ raise __NotImplementedError(format)
+
+ annotations = annotationlib.call_annotate_function(
+ annotate,
+ Format.STRING,
+ )
+
+ self.assertEqual(annotations, {"x": "str"})
+
+ def test_condition_not_stringified(self):
+ # Make sure the first condition isn't evaluated as True by being
converted
+ # to a _Stringifier
+ def annotate(format, /):
+ if format == Format.FORWARDREF:
+ return {"x": str}
+ else:
+ raise NotImplementedError(format)
+
+ with self.assertRaises(NotImplementedError):
+ annotationlib.call_annotate_function(annotate, Format.STRING)
+
+ def test_error_from_value_raised(self):
+ # Test that the error from format.VALUE is raised
+ # if all formats fail
+
+ class DemoException(Exception): ...
+
+ def annotate(format, /):
+ if format == Format.VALUE:
+ raise DemoException()
+ else:
+ raise NotImplementedError(format)
+
+ for fmt in [Format.VALUE, Format.FORWARDREF, Format.STRING]:
+ with self.assertRaises(DemoException):
+ annotationlib.call_annotate_function(annotate, format=fmt)
+
+
class MetaclassTests(unittest.TestCase):
def test_annotated_meta(self):
class Meta(type):
diff --git
a/Misc/NEWS.d/next/Library/2025-09-12-09-34-37.gh-issue-138764.mokHoY.rst
b/Misc/NEWS.d/next/Library/2025-09-12-09-34-37.gh-issue-138764.mokHoY.rst
new file mode 100644
index 00000000000000..85ebef8ff11d5c
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-09-12-09-34-37.gh-issue-138764.mokHoY.rst
@@ -0,0 +1,3 @@
+Prevent :func:`annotationlib.call_annotate_function` from calling
``__annotate__`` functions that don't support ``VALUE_WITH_FAKE_GLOBALS`` in a
fake globals namespace with empty globals.
+
+Make ``FORWARDREF`` and ``STRING`` annotations fall back to using ``VALUE``
annotations in the case that neither their own format, nor
``VALUE_WITH_FAKE_GLOBALS`` are supported.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]