diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index 3283fde9e12a8a..c798b11f5aa56e 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__ diff --git a/Lib/test/test_type_annotations.py b/Lib/test/test_type_annotations.py index 7d88f4cdfa3141..0afcd76af153e7 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)]") @@ -400,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 @@ -408,3 +415,45 @@ 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"}) + + 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__ diff --git a/Python/codegen.c b/Python/codegen.c index 6d3272edfdbf94..7432415b17414e 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -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;