Skip to content

Commit 06b6228

Browse files
authored
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()`
1 parent 12837c6 commit 06b6228

File tree

1 file changed

+69
-1
lines changed

1 file changed

+69
-1
lines changed

Lib/test/test_annotationlib.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import pickle
1010
from string.templatelib import Template, Interpolation
1111
import typing
12+
import sys
1213
import unittest
1314
from annotationlib import (
1415
Format,
@@ -755,6 +756,8 @@ def test_stringized_annotations_in_module(self):
755756

756757
for kwargs in [
757758
{"eval_str": True},
759+
{"eval_str": True, "globals": isa.__dict__, "locals": {}},
760+
{"eval_str": True, "globals": {}, "locals": isa.__dict__},
758761
{"format": Format.VALUE, "eval_str": True},
759762
]:
760763
with self.subTest(**kwargs):
@@ -788,7 +791,7 @@ def test_stringized_annotations_in_empty_module(self):
788791
self.assertEqual(get_annotations(isa2, eval_str=False), {})
789792

790793
def test_stringized_annotations_with_star_unpack(self):
791-
def f(*args: *tuple[int, ...]): ...
794+
def f(*args: "*tuple[int, ...]"): ...
792795
self.assertEqual(get_annotations(f, eval_str=True),
793796
{'args': (*tuple[int, ...],)[0]})
794797

@@ -811,6 +814,44 @@ def test_stringized_annotations_on_wrapper(self):
811814
{"a": "int", "b": "str", "return": "MyClass"},
812815
)
813816

817+
def test_stringized_annotations_on_partial_wrapper(self):
818+
isa = inspect_stringized_annotations
819+
820+
def times_three_str(fn: typing.Callable[[str], isa.MyClass]):
821+
@functools.wraps(fn)
822+
def wrapper(b: "str") -> "MyClass":
823+
return fn(b * 3)
824+
825+
return wrapper
826+
827+
wrapped = times_three_str(functools.partial(isa.function, 1))
828+
self.assertEqual(wrapped("x"), isa.MyClass(1, "xxx"))
829+
self.assertIsNot(wrapped.__globals__, isa.function.__globals__)
830+
self.assertEqual(
831+
get_annotations(wrapped, eval_str=True),
832+
{"b": str, "return": isa.MyClass},
833+
)
834+
self.assertEqual(
835+
get_annotations(wrapped, eval_str=False),
836+
{"b": "str", "return": "MyClass"},
837+
)
838+
839+
# If functools is not loaded, names will be evaluated in the current
840+
# module instead of being unwrapped to the original.
841+
functools_mod = sys.modules["functools"]
842+
del sys.modules["functools"]
843+
844+
self.assertEqual(
845+
get_annotations(wrapped, eval_str=True),
846+
{"b": str, "return": MyClass},
847+
)
848+
self.assertEqual(
849+
get_annotations(wrapped, eval_str=False),
850+
{"b": "str", "return": "MyClass"},
851+
)
852+
853+
sys.modules["functools"] = functools_mod
854+
814855
def test_stringized_annotations_on_class(self):
815856
isa = inspect_stringized_annotations
816857
# test that local namespace lookups work
@@ -823,6 +864,16 @@ def test_stringized_annotations_on_class(self):
823864
{"x": int},
824865
)
825866

867+
def test_stringized_annotations_on_custom_object(self):
868+
class HasAnnotations:
869+
@property
870+
def __annotations__(self):
871+
return {"x": "int"}
872+
873+
ha = HasAnnotations()
874+
self.assertEqual(get_annotations(ha), {"x": "int"})
875+
self.assertEqual(get_annotations(ha, eval_str=True), {"x": int})
876+
826877
def test_stringized_annotation_permutations(self):
827878
def define_class(name, has_future, has_annos, base_text, extra_names=None):
828879
lines = []
@@ -990,6 +1041,23 @@ def __annotate__(self):
9901041
{"x": "int"},
9911042
)
9921043

1044+
def test_non_dict_annotate(self):
1045+
class WeirdAnnotate:
1046+
def __annotate__(self, *args, **kwargs):
1047+
return "not a dict"
1048+
1049+
wa = WeirdAnnotate()
1050+
for format in Format:
1051+
if format == Format.VALUE_WITH_FAKE_GLOBALS:
1052+
continue
1053+
with (
1054+
self.subTest(format=format),
1055+
self.assertRaisesRegex(
1056+
ValueError, r".*__annotate__ returned a non-dict"
1057+
),
1058+
):
1059+
get_annotations(wa, format=format)
1060+
9931061
def test_no_annotations(self):
9941062
class CustomClass:
9951063
pass

0 commit comments

Comments
 (0)