gh-132064: Make annotationlib use __annotate__ if only it is present (#132195)

This commit is contained in:
Jelle Zijlstra 2025-04-13 16:32:44 -07:00 committed by GitHub
parent a1cd4ca7f4
commit 4d3ad0467e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 126 additions and 22 deletions

View file

@ -640,12 +640,18 @@ def get_annotations(
):
"""Compute the annotations dict for an object.
obj may be a callable, class, or module.
Passing in an object of any other type raises TypeError.
obj may be a callable, class, module, or other object with
__annotate__ or __annotations__ attributes.
Passing any other object raises TypeError.
Returns a dict. get_annotations() returns a new dict every time
it's called; calling it twice on the same object will return two
different but equivalent dicts.
The *format* parameter controls the format in which annotations are returned,
and must be a member of the Format enum or its integer equivalent.
For the VALUE format, the __annotations__ is tried first; if it
does not exist, the __annotate__ function is called. The
FORWARDREF format uses __annotations__ if it exists and can be
evaluated, and otherwise falls back to calling the __annotate__ function.
The SOURCE format tries __annotate__ first, and falls back to
using __annotations__, stringified using annotations_to_string().
This function handles several details for you:
@ -687,24 +693,29 @@ def get_annotations(
match format:
case Format.VALUE:
# For VALUE, we only look at __annotations__
# For VALUE, we first look at __annotations__
ann = _get_dunder_annotations(obj)
# If it's not there, try __annotate__ instead
if ann is None:
ann = _get_and_call_annotate(obj, format)
case Format.FORWARDREF:
# For FORWARDREF, we use __annotations__ if it exists
try:
return dict(_get_dunder_annotations(obj))
ann = _get_dunder_annotations(obj)
except NameError:
pass
else:
if ann is not None:
return dict(ann)
# But if __annotations__ threw a NameError, we try calling __annotate__
ann = _get_and_call_annotate(obj, format)
if ann is not None:
return ann
# If that didn't work either, we have a very weird object: evaluating
# __annotations__ threw NameError and there is no __annotate__. In that case,
# we fall back to trying __annotations__ again.
return dict(_get_dunder_annotations(obj))
if ann is None:
# If that didn't work either, we have a very weird object: evaluating
# __annotations__ threw NameError and there is no __annotate__. In that case,
# we fall back to trying __annotations__ again.
ann = _get_dunder_annotations(obj)
case Format.STRING:
# For STRING, we try to call __annotate__
ann = _get_and_call_annotate(obj, format)
@ -712,12 +723,18 @@ def get_annotations(
return ann
# But if we didn't get it, we use __annotations__ instead.
ann = _get_dunder_annotations(obj)
return annotations_to_string(ann)
if ann is not None:
ann = annotations_to_string(ann)
case Format.VALUE_WITH_FAKE_GLOBALS:
raise ValueError("The VALUE_WITH_FAKE_GLOBALS format is for internal use only")
case _:
raise ValueError(f"Unsupported format {format!r}")
if ann is None:
if isinstance(obj, type) or callable(obj):
return {}
raise TypeError(f"{obj!r} does not have annotations")
if not ann:
return {}
@ -746,10 +763,8 @@ def get_annotations(
obj_globals = getattr(obj, "__globals__", None)
obj_locals = None
unwrap = obj
elif ann is not None:
obj_globals = obj_locals = unwrap = None
else:
raise TypeError(f"{obj!r} is not a module, class, or callable.")
obj_globals = obj_locals = unwrap = None
if unwrap is not None:
while True:
@ -827,11 +842,11 @@ def _get_dunder_annotations(obj):
ann = obj.__annotations__
except AttributeError:
# For static types, the descriptor raises AttributeError.
return {}
return None
else:
ann = getattr(obj, "__annotations__", None)
if ann is None:
return {}
return None
if not isinstance(ann, dict):
raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None")