From ddacdb7d177b4a8bf2415f4fefee756bb8f94e31 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 23 Sep 2024 23:40:27 -0700 Subject: [PATCH] gh-119180: Add VALUE_WITH_FAKE_GLOBALS format to annotationlib --- Doc/library/annotationlib.rst | 11 +++++++++++ Lib/annotationlib.py | 7 +++++-- Lib/test/test_annotationlib.py | 14 +++++++++++++- Lib/typing.py | 15 ++++++++++----- Python/codegen.c | 10 +++++++++- 5 files changed, 48 insertions(+), 9 deletions(-) diff --git a/Doc/library/annotationlib.rst b/Doc/library/annotationlib.rst index 1e72c5421674bc..868672e267fa09 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/Lib/annotationlib.py b/Lib/annotationlib.py index 0a67742a2b3081..bf16ebbf9e69a1 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -20,6 +20,7 @@ class Format(enum.IntEnum): VALUE = 1 FORWARDREF = 2 SOURCE = 3 + VALUE_WITH_FAKE_GLOBALS = 4 _Union = None @@ -459,6 +460,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: @@ -492,7 +495,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 { @@ -552,7 +555,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 return result diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index dd8ceb55a411fb..4bc87d24855d24 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -382,6 +382,18 @@ def f2(a: undefined): annotationlib.get_annotations(f1, format=0) with self.assertRaises(ValueError): + annotationlib.get_annotations(f1, format=42) + + 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=4) def test_custom_object_with_annotations(self): @@ -840,7 +852,7 @@ def test_pep_695_generics_with_future_annotations_nested_in_function(self): class TestCallEvaluateFunction(unittest.TestCase): def test_evaluation(self): def evaluate(format, exc=NotImplementedError): - if format != 1: + if format != 1 and format != 4: raise exc return undefined diff --git a/Lib/typing.py b/Lib/typing.py index 9377e771d60f4b..bacac8f3e4396d 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -2945,10 +2945,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 _convert_to_source(types) + match format: + case annotationlib.Format.VALUE | annotationlib.Format.FORWARDREF: + return checked_types + case annotationlib.Format.SOURCE: + return _convert_to_source(types) + case _: + raise NotImplementedError(format) return annotate @@ -3242,8 +3245,10 @@ def __annotate__(format): } elif format == annotationlib.Format.SOURCE: own = _convert_to_source(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/Python/codegen.c b/Python/codegen.c index 0305f4299aec56..19340e3f205219 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -655,13 +655,21 @@ codegen_setup_annotations_scope(compiler *c, location loc, codegen_enter_scope(c, name, COMPILE_SCOPE_ANNOTATIONS, key, loc.lineno, NULL, &umd)); - // if .format != 1: raise NotImplementedError + // if .format != 1 and .format != 4: 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]); NEW_JUMP_TARGET_LABEL(c, body); ADDOP_JUMP(c, loc, POP_JUMP_IF_FALSE, body); + + ADDOP_I(c, loc, LOAD_FAST, 0); + PyObject *four = PyLong_FromLong(4); + assert(four != NULL); + ADDOP_LOAD_CONST(c, loc, four); + ADDOP_I(c, loc, COMPARE_OP, (Py_NE << 5) | compare_masks[Py_NE]); + 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);