8000 gh-119180: Make FORWARDREF format look at __annotations__ first (#124… · python/cpython@bc54393 · GitHub
[go: up one dir, main page]

Skip to content

Commit bc54393

Browse files
gh-119180: Make FORWARDREF format look at __annotations__ first (#124479)
From discussion with Larry Hastings and Carl Meyer, this is the desired behavior.
1 parent 4e2fb7b commit bc54393

File tree

2 files changed

+143
-26
lines changed

2 files changed

+143
-26
lines changed

Lib/annotationlib.py

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -664,28 +664,38 @@ def get_annotations(
664664
if eval_str and format != Format.VALUE:
665665
raise ValueError("eval_str=True is only supported with format=Format.VALUE")
666666

667-
# For VALUE format, we look at __annotations__ directly.
668-
if format != Format.VALUE:
669-
annotate = get_annotate_function(obj)
670-
if annotate is not None:
671-
ann = call_annotate_function(annotate, format, owner=obj)
672-
if not isinstance(ann, dict):
673-
raise ValueError(f"{obj!r}.__annotate__ returned a non-dict")
674-
return dict(ann)
675-
676-
if isinstance(obj, type):
677-
try:
678-
ann = _BASE_GET_ANNOTATIONS(obj)
679-
except AttributeError:
680-
# For static types, the descriptor raises AttributeError.
681-
return {}
682-
else:
683-
ann = getattr(obj, "__annotations__", None)
684-
if ann is None:
685-
return {}
686-
687-
if not isinstance(ann, dict):
688-
raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None")
667+
match format:
668+
case Format.VALUE:
669+
# For VALUE, we only look at __annotations__
670+
ann = _get_dunder_annotations(obj)
671+
case Format.FORWARDREF:
672+
# For FORWARDREF, we use __annotations__ if it exists
673+
try:
674+
ann = _get_dunder_annotations(obj)
675+
except NameError:
676+
pass
677+
else:
678+
return dict(ann)
679+
680+
# But if __annotations__ threw a NameError, we try calling __annotate__
681+
ann = _get_and_call_annotate(obj, format)
682+
if ann is not None:
683+
return ann
684+
685+
# If that didn't work either, we have a very weird object: evaluating
686+
# __annotations__ threw NameError and there is no __annotate__. In that case,
687+
# we fall back to trying __annotations__ again.
688+
return dict(_get_dunder_annotations(obj))
689+
case Format.SOURCE:
690+
# For SOURCE, we try to call __annotate__
691+
ann = _get_and_call_annotate(obj, format)
692+
if ann is not None:
693+
return ann
694+
# But if we didn't get it, we use __annotations__ instead.
695+
ann = _get_dunder_annotations(obj)
696+
return ann
697+
case _:
698+
raise ValueError(f"Unsupported format {format!r}")
689699

690700
if not ann:
691701
return {}
@@ -750,3 +760,30 @@ def get_annotations(
750760
for key, value in ann.items()
751761
}
752762
return return_value
763+
764+
765+
def _get_and_call_annotate(obj, format):
766+
annotate = get_annotate_function(obj)
767+
if annotate is not None:
768+
ann = call_annotate_function(annotate, format, owner=obj)
769+
if not isinstance(ann, dict):
770+
raise ValueError(f"{obj!r}.__annotate__ returned a non-dict")
771+
return dict(ann)
772+
return None
773+
774+
775+
def _get_dunder_annotations(obj):
776+
if isinstance(obj, type):
777+
try:
778+
ann = _BASE_GET_ANNOTATIONS(obj)
779+
except AttributeError:
780+
# For static types, the descriptor raises AttributeError.
781+
return {}
782+
else:
783+
ann = getattr(obj, "__annotations__", None)
784+
if ann is None:
785+
return {}
786+
787+
if not isinstance(ann, dict):
788+
raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None")
789+
return dict(ann)

Lib/test/test_annotationlib.py

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -740,17 +740,97 @@ def f(x: int):
740740

741741
self.assertEqual(annotationlib.get_annotations(f), {"x": int})
742742
self.assertEqual(
743-
annotationlib.get_annotations(f, format=annotationlib.Format.FORWARDREF),
743+
annotationlib.get_annotations(f, format=Format.FORWARDREF),
744744
{"x": int},
745745
)
746746

747747
f.__annotations__["x"] = str
748748
# The modification is reflected in VALUE (the default)
749749
self.assertEqual(annotationlib.get_annotations(f), {"x": str})
750-
# ... but not in FORWARDREF, which uses __annotate__
750+
# ... and also in FORWARDREF, which tries __annotations__ if available
751751
self.assertEqual(
752-
annotationlib.get_annotations(f, format=annotationlib.Format.FORWARDREF),
753-
{"x": int},
752+
annotationlib.get_annotations(f, format=Format.FORWARDREF),
753+
{"x": str},
754+
)
755+
# ... but not in SOURCE which always uses __annotate__
756+
self.assertEqual(
757+
annotationlib.get_annotations(f, format=Format.SOURCE),
758+
{"x": "int"},
759+
)
760+
761+
def test_non_dict_annotations(self):
762+
class WeirdAnnotations:
763+
@property
764+
def __annotations__(self):
765+
return "not a dict"
766+
767+
wa = WeirdAnnotations()
768+
for format in Format:
769+
with (
770+
self.subTest(format=format),
771+
self.assertRaisesRegex(
772+
ValueError, r".*__annotations__ is neither a dict nor None"
773+
),
774+
):
775+
annotationlib.get_annotations(wa, format=format)
776+
777+
def test_annotations_on_custom_object(self):
778+
class HasAnnotations:
779+
@property
780+
def __annotations__(self):
781+
return {"x": int}
782+
783+
ha = HasAnnotations()
784+
self.assertEqual(
785+
annotationlib.get_annotations(ha, format=Format.VALUE), {"x": int}
786+
)
787+
self.assertEqual(
788+
annotationlib.get_annotations(ha, format=Format.FORWARDREF), {"x": int}
789+
)
790+
791+
# TODO(gh-124412): This should return {'x': 'int'} instead.
792+
self.assertEqual(
793+
annotationlib.get_annotations(ha, format=Format.SOURCE), {"x": int}
794+
)
795+
796+
def test_raising_annotations_on_custom_object(self):
797+
class HasRaisingAnnotations:
798+
@property
799+
def __annotations__(self):
800+
return {"x": undefined}
801+
802+
hra = HasRaisingAnnotations()
803+
804+
with self.assertRaises(NameError):
805+
annotationlib.get_annotations(hra, format=Format.VALUE)
806+
807+
with self.assertRaises(NameError):
808+
annotationlib.get_annotations(hra, format=Format.FORWARDREF)
809+
810+
undefined = float
811+
self.assertEqual(
812+
annotationlib.get_annotations(hra, format=Format.VALUE), {"x": float}
813+
)
814+
815+
def test_forwardref_prefers_annotations(self):
816+
class HasBoth:
817+
@property
818+
def __annotations__(self):
819+
return {"x": int}
820+
821+
@property
822+
def __annotate__(self):
823+
return lambda format: {"x": str}
824+
825+
hb = HasBoth()
826+
self.assertEqual(
827+
annotationlib.get_annotations(hb, format=Format.VALUE), {"x": int}
828+
)
829+
self.assertEqual(
830+
annotationlib.get_annotations(hb, format=Format.FORWARDREF), {"x": int}
831+
)
832+
self.assertEqual(
833+
annotationlib.get_annotations(hb, format=Format.SOURCE), {"x": str}
754834
)
755835

756836
def test_pep695_generic_class_with_future_annotations(self):

0 commit comments

Comments
 (0)
0