]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.14] gh-148680: Replace internal names with type_reprs of objects in string represe...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Thu, 23 Apr 2026 13:47:52 +0000 (15:47 +0200)
committerGitHub <noreply@github.com>
Thu, 23 Apr 2026 13:47:52 +0000 (13:47 +0000)
(cherry picked from commit 158dbbb97fffbc47eb446d2b1576ce887e5c1802)

Co-authored-by: David Ellis <ducksual@gmail.com>
Co-authored-by: Shamil <ashm.tech@proton.me>
Lib/annotationlib.py
Lib/test/test_annotationlib.py
Misc/NEWS.d/next/Library/2026-04-23-07-38-04.gh-issue-148680.___ePl.rst [new file with mode: 0644]

index 9fee2564114339017c0b77829cea9e0d255270e5..5c9a0812646f8149eb4afa4da1fdaafeb5422bf3 100644 (file)
@@ -47,6 +47,7 @@ _SLOTS = (
     "__cell__",
     "__owner__",
     "__stringifier_dict__",
+    "__resolved_str_cache__",
 )
 
 
@@ -94,6 +95,7 @@ class ForwardRef:
         # value later.
         self.__code__ = None
         self.__ast_node__ = None
+        self.__resolved_str_cache__ = None
 
     def __init_subclass__(cls, /, *args, **kwds):
         raise TypeError("Cannot subclass ForwardRef")
@@ -113,7 +115,7 @@ class ForwardRef:
         """
         match format:
             case Format.STRING:
-                return self.__forward_arg__
+                return self.__resolved_str__
             case Format.VALUE:
                 is_forwardref_format = False
             case Format.FORWARDREF:
@@ -258,6 +260,24 @@ class ForwardRef:
             "Attempted to access '__forward_arg__' on an uninitialized ForwardRef"
         )
 
+    @property
+    def __resolved_str__(self):
+        # __forward_arg__ with any names from __extra_names__ replaced
+        # with the type_repr of the value they represent
+        if self.__resolved_str_cache__ is None:
+            resolved_str = self.__forward_arg__
+            names = self.__extra_names__
+
+            if names:
+                visitor = _ExtraNameFixer(names)
+                ast_expr = ast.parse(resolved_str, mode="eval").body
+                node = visitor.visit(ast_expr)
+                resolved_str = ast.unparse(node)
+
+            self.__resolved_str_cache__ = resolved_str
+
+        return self.__resolved_str_cache__
+
     @property
     def __forward_code__(self):
         if self.__code__ is not None:
@@ -321,7 +341,7 @@ class ForwardRef:
             extra.append(", is_class=True")
         if self.__owner__ is not None:
             extra.append(f", owner={self.__owner__!r}")
-        return f"ForwardRef({self.__forward_arg__!r}{''.join(extra)})"
+        return f"ForwardRef({self.__resolved_str__!r}{''.join(extra)})"
 
 
 _Template = type(t"")
@@ -357,6 +377,7 @@ class _Stringifier:
         self.__cell__ = cell
         self.__owner__ = owner
         self.__stringifier_dict__ = stringifier_dict
+        self.__resolved_str_cache__ = None  # Needed for ForwardRef
 
     def __convert_to_ast(self, other):
         if isinstance(other, _Stringifier):
@@ -1163,3 +1184,14 @@ def _get_dunder_annotations(obj):
     if not isinstance(ann, dict):
         raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None")
     return ann
+
+
+class _ExtraNameFixer(ast.NodeTransformer):
+    """Fixer for __extra_names__ items in ForwardRef __repr__ and string evaluation"""
+    def __init__(self, extra_names):
+        self.extra_names = extra_names
+
+    def visit_Name(self, node: ast.Name):
+        if (new_name := self.extra_names.get(node.id, _sentinel)) is not _sentinel:
+            node = ast.Name(id=type_repr(new_name))
+        return node
index 50cf8fcb6b4ed60f29cac7a0349f72c6bc539c75..77f2a77882fce25bad49ba2b2f9a0d49f24e5594 100644 (file)
@@ -1961,6 +1961,15 @@ class TestForwardRefClass(unittest.TestCase):
             "typing.List[ForwardRef('int', owner='class')]",
         )
 
+    def test_forward_repr_extra_names(self):
+        def f(a: undefined | str): ...
+
+        annos = get_annotations(f, format=Format.FORWARDREF)
+
+        self.assertRegex(
+            repr(annos['a']), r"ForwardRef\('undefined \| str'.*\)"
+        )
+
     def test_forward_recursion_actually(self):
         def namespace1():
             a = ForwardRef("A")
@@ -2037,6 +2046,17 @@ class TestForwardRefClass(unittest.TestCase):
         fr = ForwardRef("set[Any]")
         self.assertEqual(fr.evaluate(format=Format.STRING), "set[Any]")
 
+    def test_evaluate_string_format_extra_names(self):
+        # Test that internal extra_names are replaced when evaluating as strings
+        def f(a: unknown | str | int | list[str] | tuple[int, ...]): ...
+
+        fr = get_annotations(f, format=Format.FORWARDREF)['a']
+        # Test the cache is not populated before access
+        self.assertIsNone(fr.__resolved_str_cache__)
+
+        self.assertEqual(fr.evaluate(format=Format.STRING), "unknown | str | int | list[str] | tuple[int, ...]")
+        self.assertEqual(fr.__resolved_str_cache__, "unknown | str | int | list[str] | tuple[int, ...]")
+
     def test_evaluate_forwardref_format(self):
         fr = ForwardRef("undef")
         evaluated = fr.evaluate(format=Format.FORWARDREF)
diff --git a/Misc/NEWS.d/next/Library/2026-04-23-07-38-04.gh-issue-148680.___ePl.rst b/Misc/NEWS.d/next/Library/2026-04-23-07-38-04.gh-issue-148680.___ePl.rst
new file mode 100644 (file)
index 0000000..d379007
--- /dev/null
@@ -0,0 +1 @@
+``ForwardRef`` objects that contain internal names to represent known objects now show the ``type_repr`` of the known object rather than the internal ``__annotationlib_name_x__`` name when evaluated as strings.