From dcf629213bc046318c862ec0af5db3dfd1fc473a Mon Sep 17 00:00:00 2001
From: Jelle Zijlstra <jelle.zijlstra@gmail.com>
Date: Tue, 26 Nov 2024 07:40:13 -0800
Subject: gh-119180: Add VALUE_WITH_FAKE_GLOBALS format to annotationlib
 (#124415)

---
 Doc/library/annotationlib.rst     | 11 +++++++++++
 Include/internal/pycore_object.h  |  7 +++++++
 Lib/annotationlib.py              | 13 +++++++++----
 Lib/test/test_annotationlib.py    | 36 ++++++++++++++++++++++++++----------
 Lib/test/test_type_annotations.py |  4 ++--
 Lib/typing.py                     | 15 ++++++++++-----
 Objects/typevarobject.c           |  4 ++--
 Python/codegen.c                  |  9 ++++++---
 8 files changed, 73 insertions(+), 26 deletions(-)

diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst
index 3749045..dcaff3d 100644
--- a/Doc/library/annotationlib.rst
+++ b/Doc/library/annotationlib.rst
@@ -144,6 +144,17 @@ Classes
 
       The exact values of these strings may change in future versions of Python.
 
+   .. attribute:: VALUE_WITH_FAKE_GLOBALS
+      :value: 4
+
+      Special value used to signal that an annotate function is being
+      evaluated in a special environment with fake globals. When passed this
+      value, annotate functions should either return the same value as for
+      the :attr:`Format.VALUE` format, or raise :exc:`NotImplementedError`
+      to signal that they do not support execution in this environment.
+      This format is only used internally and should not be passed to
+      the functions in this module.
+
    .. versionadded:: 3.14
 
 .. class:: ForwardRef
diff --git a/Include/internal/pycore_object.h b/Include/internal/pycore_object.h
index 783d88c..34d835a 100644
--- a/Include/internal/pycore_object.h
+++ b/Include/internal/pycore_object.h
@@ -919,6 +919,13 @@ PyAPI_DATA(int) _Py_SwappedOp[];
 
 extern void _Py_GetConstant_Init(void);
 
+enum _PyAnnotateFormat {
+    _Py_ANNOTATE_FORMAT_VALUE = 1,
+    _Py_ANNOTATE_FORMAT_VALUE_WITH_FAKE_GLOBALS = 2,
+    _Py_ANNOTATE_FORMAT_FORWARDREF = 3,
+    _Py_ANNOTATE_FORMAT_STRING = 4,
+};
+
 #ifdef __cplusplus
 }
 #endif
diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py
index 732fbfa..42f1f38 100644
--- a/Lib/annotationlib.py
+++ b/Lib/annotationlib.py
@@ -22,8 +22,9 @@ __all__ = [
 
 class Format(enum.IntEnum):
     VALUE = 1
-    FORWARDREF = 2
-    STRING = 3
+    VALUE_WITH_FAKE_GLOBALS = 2
+    FORWARDREF = 3
+    STRING = 4
 
 
 _Union = None
@@ -513,6 +514,8 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
     on the generated ForwardRef objects.
 
     """
+    if format == Format.VALUE_WITH_FAKE_GLOBALS:
+        raise ValueError("The VALUE_WITH_FAKE_GLOBALS format is for internal use only")
     try:
         return annotate(format)
     except NotImplementedError:
@@ -546,7 +549,7 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
             argdefs=annotate.__defaults__,
             kwdefaults=annotate.__kwdefaults__,
         )
-        annos = func(Format.VALUE)
+        annos = func(Format.VALUE_WITH_FAKE_GLOBALS)
         if _is_evaluate:
             return annos if isinstance(annos, str) else repr(annos)
         return {
@@ -607,7 +610,7 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
             argdefs=annotate.__defaults__,
             kwdefaults=annotate.__kwdefaults__,
         )
-        result = func(Format.VALUE)
+        result = func(Format.VALUE_WITH_FAKE_GLOBALS)
         for obj in globals.stringifiers:
             obj.__class__ = ForwardRef
             obj.__stringifier_dict__ = None  # not needed for ForwardRef
@@ -726,6 +729,8 @@ def get_annotations(
             # But if we didn't get it, we use __annotations__ instead.
             ann = _get_dunder_annotations(obj)
             return annotations_to_string(ann)
+        case Format.VALUE_WITH_FAKE_GLOBALS:
+            raise ValueError("The VALUE_WITH_FAKE_GLOBALS format is for internal use only")
         case _:
             raise ValueError(f"Unsupported format {format!r}")
 
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index 2ca7058..20f74b4 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -42,11 +42,14 @@ class TestFormat(unittest.TestCase):
         self.assertEqual(Format.VALUE.value, 1)
         self.assertEqual(Format.VALUE, 1)
 
-        self.assertEqual(Format.FORWARDREF.value, 2)
-        self.assertEqual(Format.FORWARDREF, 2)
+        self.assertEqual(Format.VALUE_WITH_FAKE_GLOBALS.value, 2)
+        self.assertEqual(Format.VALUE_WITH_FAKE_GLOBALS, 2)
 
-        self.assertEqual(Format.STRING.value, 3)
-        self.assertEqual(Format.STRING, 3)
+        self.assertEqual(Format.FORWARDREF.value, 3)
+        self.assertEqual(Format.FORWARDREF, 3)
+
+        self.assertEqual(Format.STRING.value, 4)
+        self.assertEqual(Format.STRING, 4)
 
 
 class TestForwardRefFormat(unittest.TestCase):
@@ -459,19 +462,28 @@ class TestGetAnnotations(unittest.TestCase):
             annotationlib.get_annotations(f2, format=Format.FORWARDREF),
             {"a": fwd},
         )
-        self.assertEqual(annotationlib.get_annotations(f2, format=2), {"a": fwd})
+        self.assertEqual(annotationlib.get_annotations(f2, format=3), {"a": fwd})
 
         self.assertEqual(
             annotationlib.get_annotations(f1, format=Format.STRING),
             {"a": "int"},
         )
-        self.assertEqual(annotationlib.get_annotations(f1, format=3), {"a": "int"})
+        self.assertEqual(annotationlib.get_annotations(f1, format=4), {"a": "int"})
 
         with self.assertRaises(ValueError):
-            annotationlib.get_annotations(f1, format=0)
+            annotationlib.get_annotations(f1, format=42)
 
-        with self.assertRaises(ValueError):
-            annotationlib.get_annotations(f1, format=4)
+        with self.assertRaisesRegex(
+            ValueError,
+            r"The VALUE_WITH_FAKE_GLOBALS format is for internal use only",
+        ):
+            annotationlib.get_annotations(f1, format=Format.VALUE_WITH_FAKE_GLOBALS)
+
+        with self.assertRaisesRegex(
+            ValueError,
+            r"The VALUE_WITH_FAKE_GLOBALS format is for internal use only",
+        ):
+            annotationlib.get_annotations(f1, format=2)
 
     def test_custom_object_with_annotations(self):
         class C:
@@ -505,6 +517,8 @@ class TestGetAnnotations(unittest.TestCase):
 
         foo.__annotations__ = {"a": "foo", "b": "str"}
         for format in Format:
+            if format is Format.VALUE_WITH_FAKE_GLOBALS:
+                continue
             with self.subTest(format=format):
                 self.assertEqual(
                     annotationlib.get_annotations(foo, format=format),
@@ -802,6 +816,8 @@ class TestGetAnnotations(unittest.TestCase):
 
         wa = WeirdAnnotations()
         for format in Format:
+            if format is Format.VALUE_WITH_FAKE_GLOBALS:
+                continue
             with (
                 self.subTest(format=format),
                 self.assertRaisesRegex(
@@ -990,7 +1006,7 @@ class TestGetAnnotations(unittest.TestCase):
 class TestCallEvaluateFunction(unittest.TestCase):
     def test_evaluation(self):
         def evaluate(format, exc=NotImplementedError):
-            if format != 1:
+            if format > 2:
                 raise exc
             return undefined
 
diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py
index 257b7fa..7d88f4c 100644
--- a/Lib/test/test_type_annotations.py
+++ b/Lib/test/test_type_annotations.py
@@ -316,7 +316,7 @@ class DeferredEvaluationTests(unittest.TestCase):
         ns = run_code("x: undefined = 1")
         anno = ns["__annotate__"]
         with self.assertRaises(NotImplementedError):
-            anno(2)
+            anno(3)
 
         with self.assertRaises(NameError):
             anno(1)
@@ -376,7 +376,7 @@ class DeferredEvaluationTests(unittest.TestCase):
                     annotate(annotationlib.Format.FORWARDREF)
                 with self.assertRaises(NotImplementedError):
                     annotate(annotationlib.Format.STRING)
-                with self.assertRaises(NotImplementedError):
+                with self.assertRaises(TypeError):
                     annotate(None)
                 self.assertEqual(annotate(annotationlib.Format.VALUE), {"x": int})
 
diff --git a/Lib/typing.py b/Lib/typing.py
index 938e529..5f3aacd 100644
--- a/Lib/typing.py
+++ b/Lib/typing.py
@@ -2936,10 +2936,13 @@ def _make_eager_annotate(types):
     checked_types = {key: _type_check(val, f"field {key} annotation must be a type")
                      for key, val in types.items()}
     def annotate(format):
-        if format in (annotationlib.Format.VALUE, annotationlib.Format.FORWARDREF):
-            return checked_types
-        else:
-            return annotationlib.annotations_to_string(types)
+        match format:
+            case annotationlib.Format.VALUE | annotationlib.Format.FORWARDREF:
+                return checked_types
+            case annotationlib.Format.STRING:
+                return annotationlib.annotations_to_string(types)
+            case _:
+                raise NotImplementedError(format)
     return annotate
 
 
@@ -3229,8 +3232,10 @@ class _TypedDictMeta(type):
                     }
             elif format == annotationlib.Format.STRING:
                 own = annotationlib.annotations_to_string(own_annotations)
-            else:
+            elif format in (annotationlib.Format.FORWARDREF, annotationlib.Format.VALUE):
                 own = own_checked_annotations
+            else:
+                raise NotImplementedError(format)
             annos.update(own)
             return annos
 
diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c
index bacb858..4ed40aa 100644
--- a/Objects/typevarobject.c
+++ b/Objects/typevarobject.c
@@ -1,6 +1,6 @@
 // TypeVar, TypeVarTuple, and ParamSpec
 #include "Python.h"
-#include "pycore_object.h"        // _PyObject_GC_TRACK/UNTRACK
+#include "pycore_object.h"        // _PyObject_GC_TRACK/UNTRACK, PyAnnotateFormat
 #include "pycore_typevarobject.h"
 #include "pycore_unionobject.h"   // _Py_union_type_or
 
@@ -168,7 +168,7 @@ constevaluator_call(PyObject *self, PyObject *args, PyObject *kwargs)
         return NULL;
     }
     PyObject *value = ((constevaluatorobject *)self)->value;
-    if (format == 3) { // STRING
+    if (format == _Py_ANNOTATE_FORMAT_STRING) {
         PyUnicodeWriter *writer = PyUnicodeWriter_Create(5);  // cannot be <5
         if (writer == NULL) {
             return NULL;
diff --git a/Python/codegen.c b/Python/codegen.c
index dbf36cd..a5e550c 100644
--- a/Python/codegen.c
+++ b/Python/codegen.c
@@ -24,6 +24,7 @@
 #include "pycore_instruction_sequence.h" // _PyInstructionSequence_NewLabel()
 #include "pycore_intrinsics.h"
 #include "pycore_long.h"          // _PyLong_GetZero()
+#include "pycore_object.h"        // _Py_ANNOTATE_FORMAT_VALUE_WITH_FAKE_GLOBALS
 #include "pycore_pystate.h"       // _Py_GetConfig()
 #include "pycore_symtable.h"      // PySTEntryObject
 
@@ -672,14 +673,16 @@ codegen_setup_annotations_scope(compiler *c, location loc,
         codegen_enter_scope(c, name, COMPILE_SCOPE_ANNOTATIONS,
                             key, loc.lineno, NULL, &umd));
 
+    // if .format > VALUE_WITH_FAKE_GLOBALS: raise NotImplementedError
+    PyObject *value_with_fake_globals = PyLong_FromLong(_Py_ANNOTATE_FORMAT_VALUE_WITH_FAKE_GLOBALS);
     assert(!SYMTABLE_ENTRY(c)->ste_has_docstring);
-    // if .format != 1: raise NotImplementedError
     _Py_DECLARE_STR(format, ".format");
     ADDOP_I(c, loc, LOAD_FAST, 0);
-    ADDOP_LOAD_CONST(c, loc, _PyLong_GetOne());
-    ADDOP_I(c, loc, COMPARE_OP, (Py_NE << 5) | compare_masks[Py_NE]);
+    ADDOP_LOAD_CONST(c, loc, value_with_fake_globals);
+    ADDOP_I(c, loc, COMPARE_OP, (Py_GT << 5) | compare_masks[Py_GT]);
     NEW_JUMP_TARGET_LABEL(c, body);
     ADDOP_JUMP(c, loc, POP_JUMP_IF_FALSE, body);
+
     ADDOP_I(c, loc, LOAD_COMMON_CONSTANT, CONSTANT_NOTIMPLEMENTEDERROR);
     ADDOP_I(c, loc, RAISE_VARARGS, 1);
     USE_LABEL(c, body);
-- 
cgit v0.12