From 28d0422d11f4d24ddb020953d7f24132ceeb7d37 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sat, 23 Aug 2025 08:03:52 +0930 Subject: [PATCH 1/6] Fix `ForwardRef` double annotation evaluation --- Lib/annotationlib.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index bee019cd51591e..4706a35e8e7901 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -159,12 +159,12 @@ def evaluate( type_params = getattr(owner, "__type_params__", None) # Type parameters exist in their own scope, which is logically - # between the locals and the globals. We simulate this by adding - # them to the globals. + # between the locals and the globals. + type_param_scope = {} if type_params is not None: - globals = dict(globals) for param in type_params: - globals[param.__name__] = param + type_param_scope[param.__name__] = param + if self.__extra_names__: locals = {**locals, **self.__extra_names__} @@ -172,6 +172,8 @@ def evaluate( if arg.isidentifier() and not keyword.iskeyword(arg): if arg in locals: return locals[arg] + elif arg in type_param_scope: + return type_param_scope[arg] elif arg in globals: return globals[arg] elif hasattr(builtins, arg): @@ -183,12 +185,12 @@ def evaluate( else: code = self.__forward_code__ try: - return eval(code, globals=globals, locals=locals) + return eval(code, globals=globals, locals={**type_param_scope, **locals}) except Exception: if not is_forwardref_format: raise new_locals = _StringifierDict( - {**builtins.__dict__, **locals}, + {**type_param_scope, **builtins.__dict__, **locals}, globals=globals, owner=owner, is_class=self.__forward_is_class__, From 8170a31d62dda3fe25db81401e1775575ac8700e Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 23:50:39 +0000 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-08-22-23-50-38.gh-issue-137969.Fkvis3.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-08-22-23-50-38.gh-issue-137969.Fkvis3.rst diff --git a/Misc/NEWS.d/next/Library/2025-08-22-23-50-38.gh-issue-137969.Fkvis3.rst b/Misc/NEWS.d/next/Library/2025-08-22-23-50-38.gh-issue-137969.Fkvis3.rst new file mode 100644 index 00000000000000..59f9e6e3d331ec --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-08-22-23-50-38.gh-issue-137969.Fkvis3.rst @@ -0,0 +1,2 @@ +Fix :meth:`annotationlib.ForwardRef.evaluate` returning :class:`annotationlib.ForwardRef` +objects which do not update in new contexts. From 1f23c411fc98904315fdcb3b49c72701bdd2ce6a Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sat, 23 Aug 2025 10:10:32 +0930 Subject: [PATCH 3/6] Add test for re-evaluation of `ForwardRef` objects --- Lib/test/test_annotationlib.py | 49 +++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 88e0d611647f28..ad99945a38e6e4 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -1,30 +1,25 @@ """Tests for the annotations module.""" -import textwrap -import annotationlib import builtins import collections import functools import itertools import pickle -from string.templatelib import Template +import textwrap import typing import unittest -from annotationlib import ( - Format, - ForwardRef, - get_annotations, - annotations_to_string, - type_repr, -) -from typing import Unpack, get_type_hints, List, Union - +from string.templatelib import Template from test import support from test.support import import_helper -from test.test_inspect import inspect_stock_annotations -from test.test_inspect import inspect_stringized_annotations -from test.test_inspect import inspect_stringized_annotations_2 -from test.test_inspect import inspect_stringized_annotations_pep695 +from test.test_inspect import (inspect_stock_annotations, + inspect_stringized_annotations, + inspect_stringized_annotations_2, + inspect_stringized_annotations_pep695) +from typing import List, Union, Unpack, get_type_hints + +import annotationlib +from annotationlib import (Format, ForwardRef, annotations_to_string, + get_annotations, type_repr) def times_three(fn): @@ -1683,6 +1678,28 @@ def test_fwdref_invalid_syntax(self): with self.assertRaises(SyntaxError): fr.evaluate() + def test_re_evaluate(self): + class C: + x: alias + + evaluated = get_annotations(C, format=Format.FORWARDREF)["x"].evaluate(format=Format.FORWARDREF) + alias = int + self.assertIs(evaluated.evaluate(), int) + + del alias + evaluated = get_annotations(C, format=Format.FORWARDREF)["x"].evaluate(format=Format.FORWARDREF) + with self.assertRaises(NameError): + evaluated.evaluate() + + class C: + x: alias2 + + evaluated = get_annotations(C, format=Format.FORWARDREF)["x"].evaluate(format=Format.FORWARDREF) + global alias2 + alias2 = str + self.assertIs(evaluated.evaluate(), str) + + class TestAnnotationLib(unittest.TestCase): def test__all__(self): From c6065ad784dc55d0017760955353e7aaf0e21fd7 Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sat, 23 Aug 2025 15:41:05 +0930 Subject: [PATCH 4/6] Revert accidental formatting changes --- Lib/test/test_annotationlib.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index ad99945a38e6e4..3710558b36690a 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -1,25 +1,30 @@ """Tests for the annotations module.""" +import textwrap +import annotationlib import builtins import collections import functools import itertools import pickle -import textwrap +from string.templatelib import Template import typing import unittest -from string.templatelib import Template +from annotationlib import ( + Format, + ForwardRef, + get_annotations, + annotations_to_string, + type_repr, +) +from typing import Unpack, get_type_hints, List, Union + from test import support from test.support import import_helper -from test.test_inspect import (inspect_stock_annotations, - inspect_stringized_annotations, - inspect_stringized_annotations_2, - inspect_stringized_annotations_pep695) -from typing import List, Union, Unpack, get_type_hints - -import annotationlib -from annotationlib import (Format, ForwardRef, annotations_to_string, - get_annotations, type_repr) +from test.test_inspect import inspect_stock_annotations +from test.test_inspect import inspect_stringized_annotations +from test.test_inspect import inspect_stringized_annotations_2 +from test.test_inspect import inspect_stringized_annotations_pep695 def times_three(fn): @@ -1700,7 +1705,6 @@ class C: self.assertIs(evaluated.evaluate(), str) - class TestAnnotationLib(unittest.TestCase): def test__all__(self): support.check__all__(self, annotationlib) From 558d6db3e9de23c28e165d1350de983dcba28b1d Mon Sep 17 00:00:00 2001 From: dr-carlos Date: Sat, 23 Aug 2025 18:13:45 +0930 Subject: [PATCH 5/6] Update re-evaluate test to test generic type --- Lib/test/test_annotationlib.py | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 3710558b36690a..7db5dd6620d8f7 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -1683,26 +1683,14 @@ def test_fwdref_invalid_syntax(self): with self.assertRaises(SyntaxError): fr.evaluate() - def test_re_evaluate(self): + def test_re_evaluate_generics(self): + global alias class C: - x: alias + x: alias[int] evaluated = get_annotations(C, format=Format.FORWARDREF)["x"].evaluate(format=Format.FORWARDREF) - alias = int - self.assertIs(evaluated.evaluate(), int) - - del alias - evaluated = get_annotations(C, format=Format.FORWARDREF)["x"].evaluate(format=Format.FORWARDREF) - with self.assertRaises(NameError): - evaluated.evaluate() - - class C: - x: alias2 - - evaluated = get_annotations(C, format=Format.FORWARDREF)["x"].evaluate(format=Format.FORWARDREF) - global alias2 - alias2 = str - self.assertIs(evaluated.evaluate(), str) + alias = list + self.assertEqual(evaluated.evaluate(), list[int]) class TestAnnotationLib(unittest.TestCase): From a8c4606a69886977858449bc61d97a0770c6c368 Mon Sep 17 00:00:00 2001 From: dr-carlos <77367421+dr-carlos@users.noreply.github.com> Date: Sat, 6 Sep 2025 08:06:32 +0930 Subject: [PATCH 6/6] Ensure that type params overwrite builtins Co-authored-by: Jelle Zijlstra --- Lib/annotationlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 4706a35e8e7901..d206aff8696ccd 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -190,7 +190,7 @@ def evaluate( if not is_forwardref_format: raise new_locals = _StringifierDict( - {**type_param_scope, **builtins.__dict__, **locals}, + {**builtins.__dict__, **type_param_scope, **locals}, globals=globals, owner=owner, is_class=self.__forward_is_class__,