From 02770d72dcc02afb3d62063bf847a6ee56c9f3f8 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 27 Sep 2024 18:50:46 -0700 Subject: [PATCH 1/5] gh-119180: Set the name of the param to __annotate__ to "format" This is what Larry wants, and so it shall be. It's a bit of a hack, but it's localized and not too bad. --- Lib/test/test_type_annotations.py | 6 ++++++ Python/codegen.c | 28 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 257b7fa95dcb76..865b66e3f8c749 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -1,4 +1,5 @@ import annotationlib +import inspect import textwrap import types import unittest @@ -380,6 +381,11 @@ class X: 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)]") diff --git a/Python/codegen.c b/Python/codegen.c index 896c30cc14952a..002d1ff5006820 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -675,6 +675,34 @@ 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 so that if the name + // "format" appears in the annotations, it does not 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; From 68324ef42a73e54f383d936393ab511c5d23802a Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 28 Sep 2024 06:23:33 -0700 Subject: [PATCH 2/5] fix pydoc --- Lib/test/test_pydoc/test_pydoc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index 776e02f41a1cec..e45cb319714b82 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -79,7 +79,7 @@ class A(builtins.object) 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__ From 2dae0d0569429d7789500a45f2d3d525d5f1dcb4 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 29 Sep 2024 05:50:29 -0700 Subject: [PATCH 3/5] Add tests --- Lib/test/test_type_annotations.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 865b66e3f8c749..2a586979414e04 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -406,6 +406,7 @@ def f(x: int) -> int: pass 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 @@ -414,3 +415,21 @@ def f(x: format): pass 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"}) From 90a35be6b77150ae11e244fe4623068915799c31 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 30 Sep 2024 13:08:15 -0700 Subject: [PATCH 4/5] more tests --- Lib/test/test_type_annotations.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 2a586979414e04..e44eeaae31550c 100644 --- a/Lib/test/test_type_annotations.py +++ b/Lib/test/test_type_annotations.py @@ -433,3 +433,27 @@ def inner(x: format): pass """ 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__ From b6cbb8736518ebb795ee5124ee5acdec69be2aa3 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 30 Sep 2024 21:23:52 -0700 Subject: [PATCH 5/5] reword comment --- Python/codegen.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Python/codegen.c b/Python/codegen.c index 002d1ff5006820..db4825c17f689b 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -678,10 +678,9 @@ codegen_leave_annotations_scope(compiler *c, location loc, // 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 so that if the name - // "format" appears in the annotations, it does not get clobbered - // by this name. - // This code is essentially: + // 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) {