https://github.com/python/cpython/commit/06b62282c79dd69293a3eefb4c55f5acc6312cb2
commit: 06b62282c79dd69293a3eefb4c55f5acc6312cb2
branch: main
author: dr-carlos <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2025-11-10T14:45:22Z
summary:
gh-141174: Improve `annotationlib.get_annotations()` test coverage (#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()`
files:
M Lib/test/test_annotationlib.py
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index fd5d43b09b9702..f1d32ab50cf82b 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -9,6 +9,7 @@
import pickle
from string.templatelib import Template, Interpolation
import typing
+import sys
import unittest
from annotationlib import (
Format,
@@ -755,6 +756,8 @@ def test_stringized_annotations_in_module(self):
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 @@ def test_stringized_annotations_in_empty_module(self):
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 @@ def test_stringized_annotations_on_wrapper(self):
{"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 @@ def test_stringized_annotations_on_class(self):
{"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 @@ def __annotate__(self):
{"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
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]