Skip to content

Commit ac888cc

Browse files
committed
Support (recursively unwrap/call) any type of callable as an annotation function class' __call__ attribute
1 parent 61b76a5 commit ac888cc

File tree

2 files changed

+38
-8
lines changed

2 files changed

+38
-8
lines changed

Lib/annotationlib.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -897,10 +897,15 @@ def _get_annotate_attr(annotate, attr, default):
897897
if isinstance(annotate, types.MethodType):
898898
return _get_annotate_attr(annotate.__func__, attr, default)
899899

900-
# Class instances themselves aren't methods, their __call__ functions are.
901-
if isinstance(annotate.__call__, types.MethodType):
902-
if call_func := getattr(annotate.__call__, "__func__", None):
903-
return getattr(call_func, attr, default)
900+
# If annotate is a class instance, its __call__ is the relevant function.
901+
# However, __call__ Could be a method, a function descriptor, or any other callable.
902+
# Normal functions have a __call__ property which is a useless method wrapper,
903+
# ignore these.
904+
if (
905+
(call := getattr(annotate, "__call__", None)) and
906+
not isinstance(call, types.MethodWrapperType)
907+
):
908+
return _get_annotate_attr(annotate.__call__, attr, default)
904909

905910
# Classes and generics are callable, usually the __init__ method sets attributes,
906911
# so let's access this method for fake globals and the like.
@@ -932,12 +937,15 @@ def _direct_call_annotate(func, annotate, *args):
932937
# argument.
933938
return _direct_call_annotate(func, annotate.__func__, self, *args)
934939

935-
# If annotate is a class instance, its __call__ function is the method.
940+
# If annotate is a class instance, its __call__ is the function.
941+
# __call__ Could be a method, a function descriptor, or any other callable.
942+
# Normal functions have a __call__ property which is a useless method wrapper,
943+
# ignore these.
936944
if (
937-
hasattr(annotate.__call__, "__func__") and
938-
(self := getattr(annotate.__call__, "__self__", None))
945+
(call := getattr(annotate, "__call__", None)) and
946+
not isinstance(call, types.MethodWrapperType)
939947
):
940-
return func(self, *args)
948+
return _direct_call_annotate(func, annotate.__call__, *args)
941949

942950
# If annotate is a class, `func` is the __init__ method, so we still need to call
943951
# __new__() to create the instance

Lib/test/test_annotationlib.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1717,6 +1717,28 @@ def __init__(self, format, /, __Format=Format,
17171717

17181718
self.assertEqual(annotations, {"x": int})
17191719

1720+
def test_callable_object_custom_call_annotate_forwardref_value_fallback(self):
1721+
# If Format.STRING and Format.VALUE_WITH_FAKE_GLOBALS are not
1722+
# supported fall back to Format.VALUE and convert to strings
1723+
class AnnotateClass(dict):
1724+
def __init__(self, format, /, __Format=Format,
1725+
__NotImplementedError=NotImplementedError):
1726+
if format == __Format.VALUE:
1727+
super().__init__({"x": int})
1728+
else:
1729+
raise __NotImplementedError(format)
1730+
1731+
class Annotate:
1732+
__call__ = AnnotateClass
1733+
1734+
annotations = annotationlib.call_annotate_function(
1735+
Annotate(),
1736+
Format.FORWARDREF,
1737+
)
1738+
1739+
self.assertEqual(annotations, {"x": int})
1740+
1741+
17201742
def test_callable_generic_class_annotate_forwardref_value_fallback(self):
17211743
# Generics that inherit from builtins become types.GenericAlias objects.
17221744
# This is special-case in annotationlib to ensure the constructor is handled

0 commit comments

Comments
 (0)