]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-119180: Set the name of the param to __annotate__ to "format" (#124730)
authorJelle Zijlstra <jelle.zijlstra@gmail.com>
Mon, 30 Dec 2024 16:19:38 +0000 (08:19 -0800)
committerGitHub <noreply@github.com>
Mon, 30 Dec 2024 16:19:38 +0000 (08:19 -0800)
Lib/test/test_pydoc/test_pydoc.py
Lib/test/test_type_annotations.py
Python/codegen.c

index 3283fde9e12a8a33ffd15b7a53e7d5a3287d0121..c798b11f5aa56e32cbcc74eea656e94fb0275ecd 100644 (file)
@@ -79,7 +79,7 @@ CLASSES
     class B(builtins.object)
      |  Methods defined here:
      |
-     |  __annotate__(...)
+     |  __annotate__(format, /)
      |
      |  ----------------------------------------------------------------------
      |  Data descriptors defined here:
@@ -180,7 +180,7 @@ class A(builtins.object)
 
 class B(builtins.object)
     Methods defined here:
-        __annotate__(...)
+        __annotate__(format, /)
     ----------------------------------------------------------------------
     Data descriptors defined here:
         __dict__
index 7d88f4cdfa314105d6b52de6b94cf19946ab707e..0afcd76af153e7e4c1f80ec26dd675b7af0fa86b 100644 (file)
@@ -1,4 +1,5 @@
 import annotationlib
+import inspect
 import textwrap
 import types
 import unittest
@@ -380,6 +381,11 @@ class DeferredEvaluationTests(unittest.TestCase):
                     annotate(None)
                 self.assertEqual(annotate(annotationlib.Format.VALUE), {"x": int})
 
+                sig = inspect.signature(annotate)
+                self.assertEqual(sig, inspect.Signature([
+                    inspect.Parameter("format", inspect.Parameter.POSITIONAL_ONLY)
+                ]))
+
     def test_comprehension_in_annotation(self):
         # This crashed in an earlier version of the code
         ns = run_code("x: [y for y in range(10)]")
@@ -400,6 +406,7 @@ class DeferredEvaluationTests(unittest.TestCase):
 
     def test_name_clash_with_format(self):
         # this test would fail if __annotate__'s parameter was called "format"
+        # during symbol table construction
         code = """
         class format: pass
 
@@ -408,3 +415,45 @@ class DeferredEvaluationTests(unittest.TestCase):
         ns = run_code(code)
         f = ns["f"]
         self.assertEqual(f.__annotations__, {"x": ns["format"]})
+
+        code = """
+        class Outer:
+            class format: pass
+
+            def meth(self, x: format): ...
+        """
+        ns = run_code(code)
+        self.assertEqual(ns["Outer"].meth.__annotations__, {"x": ns["Outer"].format})
+
+        code = """
+        def f(format):
+            def inner(x: format): pass
+            return inner
+        res = f("closure var")
+        """
+        ns = run_code(code)
+        self.assertEqual(ns["res"].__annotations__, {"x": "closure var"})
+
+        code = """
+        def f(x: format):
+            pass
+        """
+        ns = run_code(code)
+        # picks up the format() builtin
+        self.assertEqual(ns["f"].__annotations__, {"x": format})
+
+        code = """
+        def outer():
+            def f(x: format):
+                pass
+            if False:
+                class format: pass
+            return f
+        f = outer()
+        """
+        ns = run_code(code)
+        with self.assertRaisesRegex(
+            NameError,
+            "cannot access free variable 'format' where it is not associated with a value in enclosing scope",
+        ):
+            ns["f"].__annotations__
index 6d3272edfdbf94aeda31846e4cc579763b791a44..7432415b17414e4cd56f5b9aeb23b715667f7e4f 100644 (file)
@@ -701,6 +701,33 @@ codegen_leave_annotations_scope(compiler *c, location loc,
     ADDOP_I(c, loc, BUILD_MAP, annotations_len);
     ADDOP_IN_SCOPE(c, loc, RETURN_VALUE);
     PyCodeObject *co = _PyCompile_OptimizeAndAssemble(c, 1);
+
+    // We want the parameter to __annotate__ to be named "format" in the
+    // signature  shown by inspect.signature(), but we need to use a
+    // different name (.format) in the symtable; if the name
+    // "format" appears in the annotations, it doesn't get clobbered
+    // by this name.  This code is essentially:
+    // co->co_localsplusnames = ("format", *co->co_localsplusnames[1:])
+    const Py_ssize_t size = PyObject_Size(co->co_localsplusnames);
+    if (size == -1) {
+        return ERROR;
+    }
+    PyObject *new_names = PyTuple_New(size);
+    if (new_names == NULL) {
+        return ERROR;
+    }
+    PyTuple_SET_ITEM(new_names, 0, Py_NewRef(&_Py_ID(format)));
+    for (int i = 1; i < size; i++) {
+        PyObject *item = PyTuple_GetItem(co->co_localsplusnames, i);
+        if (item == NULL) {
+            Py_DECREF(new_names);
+            return ERROR;
+        }
+        Py_INCREF(item);
+        PyTuple_SET_ITEM(new_names, i, item);
+    }
+    Py_SETREF(co->co_localsplusnames, new_names);
+
     _PyCompile_ExitScope(c);
     if (co == NULL) {
         return ERROR;