Skip to content

Commit 8df3d24

Browse files
committed
Support recursive unwrapping of annotate functions
1 parent e70b489 commit 8df3d24

File tree

2 files changed

+35
-2
lines changed

2 files changed

+35
-2
lines changed

Lib/annotationlib.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -905,8 +905,11 @@ def _get_annotate_attr(annotate, attr, default):
905905
if isinstance(annotate, type) or isinstance(annotate, types.GenericAlias):
906906
return getattr(annotate.__init__, attr, default)
907907

908-
if (wrapped := getattr(annotate, "__wrapped__", None)) is not None:
909-
return getattr(wrapped, attr, default)
908+
# Most 'wrapped' functions, including functools.cache and staticmethod, need us
909+
# to manually, recursively unwrap. For partial.update_wrapper functions, the
910+
# attribute is accessible on the function itself, so we never get this far.
911+
if (unwrapped := getattr(annotate, "__wrapped__", None)) is not None:
912+
return _get_annotate_attr(unwrapped, attr, default)
910913

911914
if (
912915
(functools := sys.modules.get("functools", None))

Lib/test/test_annotationlib.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1812,9 +1812,39 @@ def format(format, /, __Format=Format,
18121812
self.assertIsInstance(annotations["x"], float)
18131813
self.assertIs(annotations["y"], str)
18141814

1815+
# Check annotations again to ensure that cache is working.
18151816
new_anns = annotationlib.call_annotate_function(format, Format.FORWARDREF)
18161817
self.assertEqual(annotations, new_anns)
18171818

1819+
def test_callable_double_wrapped_annotate_forwardref_value_fallback(self):
1820+
# The raw staticmethod object returns a 'wrapped' function, and so is
1821+
# @functools.cache. Here we test that functions unwrap recursively,
1822+
# allowing wrapping of wrapped functions.
1823+
class Annotate:
1824+
@staticmethod
1825+
@functools.cache
1826+
def format(format, /, __Format=Format,
1827+
__NotImplementedError=NotImplementedError):
1828+
if format == __Format.VALUE:
1829+
return {"x": random.random(), "y": str}
1830+
else:
1831+
raise __NotImplementedError(format)
1832+
1833+
# Access the raw staticmethod object which wraps the cached function.
1834+
annotations = annotationlib.call_annotate_function(
1835+
Annotate.__dict__["format"],
1836+
Format.FORWARDREF,
1837+
)
1838+
1839+
self.assertIsInstance(annotations, dict)
1840+
self.assertIn("x", annotations)
1841+
self.assertIsInstance(annotations["x"], float)
1842+
self.assertIs(annotations["y"], str)
1843+
1844+
# Check annotations again to ensure that cache is working.
1845+
new_anns = annotationlib.call_annotate_function(Annotate.format, Format.FORWARDREF)
1846+
self.assertEqual(annotations, new_anns)
1847+
18181848
def test_callable_wrapped_annotate_forwardref_value_fallback(self):
18191849
# If Format.STRING and Format.VALUE_WITH_FAKE_GLOBALS are not
18201850
# supported fall back to Format.VALUE and convert to strings

0 commit comments

Comments
 (0)