From dcf629213bc046318c862ec0af5db3dfd1fc473a Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Tue, 26 Nov 2024 07:40:13 -0800 Subject: [PATCH] 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 37490456d13312..dcaff3d7fdbec5 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 783d88cb51ffbd..34d835a7f84ee7 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 732fbfa628cf5f..42f1f3877514d9 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -22,8 +22,9 @@ 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 2ca7058c14398c..20f74b4ed0aadb 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -42,11 +42,14 @@ def test_enum(self): 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 @@ def f2(a: undefined): 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 @@ def foo(a: int, b: str): 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 @@ def __annotations__(self): 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 @@ 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 > 2: raise exc return undefined diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 257b7fa95dcb76..7d88f4cdfa3141 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -316,7 +316,7 @@ def test_module(self): 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 X: 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 938e52922aee03..5f3aacd877221c 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 @@ def __annotate__(format): } 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 bacb858978c5d7..4ed40aa71a595e 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 dbf36cdc0b7908..a5e550cf8c947e 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);