Skip to content

Commit c4d8d9d

Browse files
committed
Support functools.cache callables in call_annotate_function()
1 parent 9e4faa4 commit c4d8d9d

File tree

2 files changed

+43
-5
lines changed

2 files changed

+43
-5
lines changed

Lib/annotationlib.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -894,9 +894,14 @@ def _get_annotate_attr(annotate, attr, default):
894894
if isinstance(annotate.__call__, types.MethodType):
895895
if call_func := getattr(annotate.__call__, "__func__", None):
896896
return getattr(call_func, attr, default)
897-
elif isinstance(annotate, type):
897+
898+
if isinstance(annotate, type):
898899
return getattr(annotate.__init__, attr, default)
899-
elif (
900+
901+
if (wrapped := getattr(annotate, "__wrapped__", None)) is not None:
902+
return getattr(wrapped, attr, default)
903+
904+
if (
900905
(functools := sys.modules.get("functools", None))
901906
and isinstance(annotate, functools.partial)
902907
):
@@ -919,13 +924,22 @@ def _direct_call_annotate(func, annotate, format):
919924
func(inst, format)
920925
return inst
921926

922-
# If annotate is a partial function, re-create it with the new function object.
923-
# We could call the function directly, but then we'd have to handle placeholders,
924-
# and this way should be more robust for future changes.
925927
if functools := sys.modules.get("functools", None):
928+
# If annotate is a partial function, re-create it with the new function object.
929+
# We could call the function directly, but then we'd have to handle placeholders,
930+
# and this way should be more robust for future changes.
926931
if isinstance(annotate, functools.partial):
927932
return functools.partial(func, *annotate.args, **annotate.keywords)(format)
928933

934+
# If annotate is a cached function, re-create it with the new function object.
935+
# We want a new, clean, cache, as we've updated the function data, so let's
936+
# re-create with the new function and old cache parameters.
937+
if isinstance(annotate, functools._lru_cache_wrapper):
938+
return functools._lru_cache_wrapper(
939+
func, **annotate.cache_parameters(),
940+
cache_info_type=(0, 0, 0, annotate.cache_parameters()["maxsize"])
941+
)(format)
942+
929943
return func(format)
930944

931945

Lib/test/test_annotationlib.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1647,6 +1647,30 @@ def format(format, second, /, *, third, __Format=Format,
16471647

16481648
self.assertEqual(annotations, {"x": Format.VALUE * 5 * 6})
16491649

1650+
def test_callable_cache_annotate_forwardref_value_fallback(self):
1651+
import random
1652+
# If Format.STRING and Format.VALUE_WITH_FAKE_GLOBALS are not
1653+
# supported fall back to Format.VALUE and convert to strings
1654+
@functools.cache
1655+
def format(format, /, __Format=Format,
1656+
__NotImplementedError=NotImplementedError):
1657+
if format == __Format.VALUE:
1658+
return {"x": random.random()}
1659+
else:
1660+
raise __NotImplementedError(format)
1661+
1662+
annotations = annotationlib.call_annotate_function(
1663+
format,
1664+
Format.FORWARDREF,
1665+
)
1666+
1667+
self.assertIsInstance(annotations, dict)
1668+
self.assertIn("x", annotations)
1669+
self.assertIsInstance(annotations["x"], float)
1670+
1671+
new_anns = annotationlib.call_annotate_function(format, Format.FORWARDREF)
1672+
self.assertEqual(annotations, new_anns)
1673+
16501674
def test_callable_object_annotate_string_fakeglobals(self):
16511675
# If Format.STRING is not supported but Format.VALUE_WITH_FAKE_GLOBALS is
16521676
# prefer that over Format.VALUE

0 commit comments

Comments
 (0)