]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.14] gh-141174: Improve `annotationlib.get_annotations()` test coverage (GH-141286...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Mon, 10 Nov 2025 15:11:42 +0000 (16:11 +0100)
committerGitHub <noreply@github.com>
Mon, 10 Nov 2025 15:11:42 +0000 (15:11 +0000)
gh-141174: Improve `annotationlib.get_annotations()` test coverage (GH-141286)

* Test `get_annotations(format=Format.VALUE)` for stringized annotations on custom objects

* Test `get_annotations(format=Format.VALUE)` for stringized annotations on wrapped partial functions

* Update test_stringized_annotations_with_star_unpack() to actually test stringized annotations

* Test __annotate__ returning a non-dict

* Test passing globals and locals to stringized `get_annotations()`
(cherry picked from commit 06b62282c79dd69293a3eefb4c55f5acc6312cb2)

Co-authored-by: dr-carlos <77367421+dr-carlos@users.noreply.github.com>
Lib/test/test_annotationlib.py

index fd5d43b09b9702a797566cd11681f79f991a8495..f1d32ab50cf82b680932e83e07da23628c70bc5d 100644 (file)
@@ -9,6 +9,7 @@ import itertools
 import pickle
 from string.templatelib import Template, Interpolation
 import typing
+import sys
 import unittest
 from annotationlib import (
     Format,
@@ -755,6 +756,8 @@ class TestGetAnnotations(unittest.TestCase):
 
         for kwargs in [
             {"eval_str": True},
+            {"eval_str": True, "globals": isa.__dict__, "locals": {}},
+            {"eval_str": True, "globals": {}, "locals": isa.__dict__},
             {"format": Format.VALUE, "eval_str": True},
         ]:
             with self.subTest(**kwargs):
@@ -788,7 +791,7 @@ class TestGetAnnotations(unittest.TestCase):
         self.assertEqual(get_annotations(isa2, eval_str=False), {})
 
     def test_stringized_annotations_with_star_unpack(self):
-        def f(*args: *tuple[int, ...]): ...
+        def f(*args: "*tuple[int, ...]"): ...
         self.assertEqual(get_annotations(f, eval_str=True),
                          {'args': (*tuple[int, ...],)[0]})
 
@@ -811,6 +814,44 @@ class TestGetAnnotations(unittest.TestCase):
             {"a": "int", "b": "str", "return": "MyClass"},
         )
 
+    def test_stringized_annotations_on_partial_wrapper(self):
+        isa = inspect_stringized_annotations
+
+        def times_three_str(fn: typing.Callable[[str], isa.MyClass]):
+            @functools.wraps(fn)
+            def wrapper(b: "str") -> "MyClass":
+                return fn(b * 3)
+
+            return wrapper
+
+        wrapped = times_three_str(functools.partial(isa.function, 1))
+        self.assertEqual(wrapped("x"), isa.MyClass(1, "xxx"))
+        self.assertIsNot(wrapped.__globals__, isa.function.__globals__)
+        self.assertEqual(
+            get_annotations(wrapped, eval_str=True),
+            {"b": str, "return": isa.MyClass},
+        )
+        self.assertEqual(
+            get_annotations(wrapped, eval_str=False),
+            {"b": "str", "return": "MyClass"},
+        )
+
+        # If functools is not loaded, names will be evaluated in the current
+        # module instead of being unwrapped to the original.
+        functools_mod = sys.modules["functools"]
+        del sys.modules["functools"]
+
+        self.assertEqual(
+            get_annotations(wrapped, eval_str=True),
+            {"b": str, "return": MyClass},
+        )
+        self.assertEqual(
+            get_annotations(wrapped, eval_str=False),
+            {"b": "str", "return": "MyClass"},
+        )
+
+        sys.modules["functools"] = functools_mod
+
     def test_stringized_annotations_on_class(self):
         isa = inspect_stringized_annotations
         # test that local namespace lookups work
@@ -823,6 +864,16 @@ class TestGetAnnotations(unittest.TestCase):
             {"x": int},
         )
 
+    def test_stringized_annotations_on_custom_object(self):
+        class HasAnnotations:
+            @property
+            def __annotations__(self):
+                return {"x": "int"}
+
+        ha = HasAnnotations()
+        self.assertEqual(get_annotations(ha), {"x": "int"})
+        self.assertEqual(get_annotations(ha, eval_str=True), {"x": int})
+
     def test_stringized_annotation_permutations(self):
         def define_class(name, has_future, has_annos, base_text, extra_names=None):
             lines = []
@@ -990,6 +1041,23 @@ class TestGetAnnotations(unittest.TestCase):
             {"x": "int"},
         )
 
+    def test_non_dict_annotate(self):
+        class WeirdAnnotate:
+            def __annotate__(self, *args, **kwargs):
+                return "not a dict"
+
+        wa = WeirdAnnotate()
+        for format in Format:
+            if format == Format.VALUE_WITH_FAKE_GLOBALS:
+                continue
+            with (
+                self.subTest(format=format),
+                self.assertRaisesRegex(
+                    ValueError, r".*__annotate__ returned a non-dict"
+                ),
+            ):
+                get_annotations(wa, format=format)
+
     def test_no_annotations(self):
         class CustomClass:
             pass