Skip to content

Commit 4d3ad04

Browse files
gh-132064: Make annotationlib use __annotate__ if only it is present (#132195)
1 parent a1cd4ca commit 4d3ad04

File tree

5 files changed

+127
-23
lines changed

5 files changed

+127
-23
lines changed

Doc/library/annotationlib.rst

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,11 +317,22 @@ Functions
317317
Compute the annotations dict for an object.
318318

319319
*obj* may be a callable, class, module, or other object with
320-
:attr:`~object.__annotate__` and :attr:`~object.__annotations__` attributes.
321-
Passing in an object of any other type raises :exc:`TypeError`.
320+
:attr:`~object.__annotate__` or :attr:`~object.__annotations__` attributes.
321+
Passing any other object raises :exc:`TypeError`.
322322

323323
The *format* parameter controls the format in which annotations are returned,
324324
and must be a member of the :class:`Format` enum or its integer equivalent.
325+
The different formats work as follows:
326+
327+
* VALUE: :attr:`!object.__annotations__` is tried first; if that does not exist,
328+
the :attr:`!object.__annotate__` function is called if it exists.
329+
* FORWARDREF: If :attr:`!object.__annotations__` exists and can be evaluated successfully,
330+
it is used; otherwise, the :attr:`!object.__annotate__` function is called. If it
331+
does not exist either, :attr:`!object.__annotations__` is tried again and any error
332+
from accessing it is re-raised.
333+
* STRING: If :attr:`!object.__annotate__` exists, it is called first;
334+
otherwise, :attr:`!object.__annotations__` is used and stringified
335+
using :func:`annotations_to_string`.
325336

326337
Returns a dict. :func:`!get_annotations` returns a new dict every time
327338
it's called; calling it twice on the same object will return two

Lib/annotationlib.py

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -640,12 +640,18 @@ def get_annotations(
640640
):
641641
"""Compute the annotations dict for an object.
642642
643-
obj may be a callable, class, or module.
644-
Passing in an object of any other type raises TypeError.
645-
646-
Returns a dict. get_annotations() returns a new dict every time
647-
it's called; calling it twice on the same object will return two
648-
different but equivalent dicts.
643+
obj may be a callable, class, module, or other object with
644+
__annotate__ or __annotations__ attributes.
645+
Passing any other object raises TypeError.
646+
647+
The *format* parameter controls the format in which annotations are returned,
648+
and must be a member of the Format enum or its integer equivalent.
649+
For the VALUE format, the __annotations__ is tried first; if it
650+
does not exist, the __annotate__ function is called. The
651+
FORWARDREF format uses __annotations__ if it exists and can be
652+
evaluated, and otherwise falls back to calling the __annotate__ function.
653+
The SOURCE format tries __annotate__ first, and falls back to
654+
using __annotations__, stringified using annotations_to_string().
649655
650656
This function handles several details for you:
651657
@@ -687,37 +693,48 @@ def get_annotations(
687693

688694
match format:
689695
case Format.VALUE:
690-
# For VALUE, we only look at __annotations__
696+
# For VALUE, we first look at __annotations__
691697
ann = _get_dunder_annotations(obj)
698+
699+
# If it's not there, try __annotate__ instead
700+
if ann is None:
701+
ann = _get_and_call_annotate(obj, format)
692702
case Format.FORWARDREF:
693703
# For FORWARDREF, we use __annotations__ if it exists
694704
try:
695-
return dict(_get_dunder_annotations(obj))
705+
ann = _get_dunder_annotations(obj)
696706
except NameError:
697707
pass
708+
else:
709+
if ann is not None:
710+
return dict(ann)
698711

699712
# But if __annotations__ threw a NameError, we try calling __annotate__
700713
ann = _get_and_call_annotate(obj, format)
701-
if ann is not None:
702-
return ann
703-
704-
# If that didn't work either, we have a very weird object: evaluating
705-
# __annotations__ threw NameError and there is no __annotate__. In that case,
706-
# we fall back to trying __annotations__ again.
707-
return dict(_get_dunder_annotations(obj))
714+
if ann is None:
715+
# If that didn't work either, we have a very weird object: evaluating
716+
# __annotations__ threw NameError and there is no __annotate__. In that case,
717+
# we fall back to trying __annotations__ again.
718+
ann = _get_dunder_annotations(obj)
708719
case Format.STRING:
709720
# For STRING, we try to call __annotate__
710721
ann = _get_and_call_annotate(obj, format)
711722
if ann is not None:
712723
return ann
713724
# But if we didn't get it, we use __annotations__ instead.
714725
ann = _get_dunder_annotations(obj)
715-
return annotations_to_string(ann)
726+
if ann is not None:
727+
ann = annotations_to_string(ann)
716728
case Format.VALUE_WITH_FAKE_GLOBALS:
717729
raise ValueError("The VALUE_WITH_FAKE_GLOBALS format is for internal use only")
718730
case _:
719731
raise ValueError(f"Unsupported format {format!r}")
720732

733+
if ann is None:
734+
if isinstance(obj, type) or callable(obj):
735+
return {}
736+
raise TypeError(f"{obj!r} does not have annotations")
737+
721738
if not ann:
722739
return {}
723740

@@ -746,10 +763,8 @@ def get_annotations(
746763
obj_globals = getattr(obj, "__globals__", None)
747764
obj_locals = None
748765
unwrap = obj
749-
elif ann is not None:
750-
obj_globals = obj_locals = unwrap = None
751766
else:
752-
raise TypeError(f"{obj!r} is not a module, class, or callable.")
767+
obj_globals = obj_locals = unwrap = None
753768

754769
if unwrap is not None:
755770
while True:
@@ -827,11 +842,11 @@ def _get_dunder_annotations(obj):
827842
ann = obj.__annotations__
828843
except AttributeError:
829844
# For static types, the descriptor raises AttributeError.
830-
return {}
845+
return None
831846
else:
832847
ann = getattr(obj, "__annotations__", None)
833848
if ann is None:
834-
return {}
849+
return None
835850

836851
if not isinstance(ann, dict):
837852
raise ValueError(f"{obj!r}.__annotations__ is neither a dict nor None")

Lib/test/test_annotationlib.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,50 @@ def __annotate__(self):
885885
annotationlib.get_annotations(hb, format=Format.STRING), {"x": str}
886886
)
887887

888+
def test_only_annotate(self):
889+
def f(x: int):
890+
pass
891+
892+
class OnlyAnnotate:
893+
@property
894+
def __annotate__(self):
895+
return f.__annotate__
896+
897+
oa = OnlyAnnotate()
898+
self.assertEqual(
899+
annotationlib.get_annotations(oa, format=Format.VALUE), {"x": int}
900+
)
901+
self.assertEqual(
902+
annotationlib.get_annotations(oa, format=Format.FORWARDREF), {"x": int}
903+
)
904+
self.assertEqual(
905+
annotationlib.get_annotations(oa, format=Format.STRING),
906+
{"x": "int"},
907+
)
908+
909+
def test_no_annotations(self):
910+
class CustomClass:
911+
pass
912+
913+
class MyCallable:
914+
def __call__(self):
915+
pass
916+
917+
for format in Format:
918+
if format == Format.VALUE_WITH_FAKE_GLOBALS:
919+
continue
920+
for obj in (None, 1, object(), CustomClass()):
921+
with self.subTest(format=format, obj=obj):
922+
with self.assertRaises(TypeError):
923+
annotationlib.get_annotations(obj, format=format)
924+
925+
# Callables and types with no annotations return an empty dict
926+
for obj in (int, len, MyCallable()):
927+
with self.subTest(format=format, obj=obj):
928+
self.assertEqual(
929+
annotationlib.get_annotations(obj, format=format), {}
930+
)
931+
888932
def test_pep695_generic_class_with_future_annotations(self):
889933
ann_module695 = inspect_stringized_annotations_pep695
890934
A_annotations = annotationlib.get_annotations(ann_module695.A, eval_str=True)

Lib/test/test_functools.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import abc
2+
from annotationlib import Format, get_annotations
23
import builtins
34
import collections
45
import collections.abc
@@ -22,6 +23,7 @@
2223

2324
from test.support import import_helper
2425
from test.support import threading_helper
26+
from test.support import EqualToForwardRef
2527

2628
import functools
2729

@@ -2075,6 +2077,34 @@ def orig(a, /, b, c=True): ...
20752077
self.assertEqual(str(Signature.from_callable(lru.cache_info)), '()')
20762078
self.assertEqual(str(Signature.from_callable(lru.cache_clear)), '()')
20772079

2080+
def test_get_annotations(self):
2081+
def orig(a: int) -> str: ...
2082+
lru = self.module.lru_cache(1)(orig)
2083+
2084+
self.assertEqual(
2085+
get_annotations(orig), {"a": int, "return": str},
2086+
)
2087+
self.assertEqual(
2088+
get_annotations(lru), {"a": int, "return": str},
2089+
)
2090+
2091+
def test_get_annotations_with_forwardref(self):
2092+
def orig(a: int) -> nonexistent: ...
2093+
lru = self.module.lru_cache(1)(orig)
2094+
2095+
self.assertEqual(
2096+
get_annotations(orig, format=Format.FORWARDREF),
2097+
{"a": int, "return": EqualToForwardRef('nonexistent', owner=orig)},
2098+
)
2099+
self.assertEqual(
2100+
get_annotations(lru, format=Format.FORWARDREF),
2101+
{"a": int, "return": EqualToForwardRef('nonexistent', owner=lru)},
2102+
)
2103+
with self.assertRaises(NameError):
2104+
get_annotations(orig, format=Format.VALUE)
2105+
with self.assertRaises(NameError):
2106+
get_annotations(lru, format=Format.VALUE)
2107+
20782108
@support.skip_on_s390x
20792109
@unittest.skipIf(support.is_wasi, "WASI has limited C stack")
20802110
@support.skip_if_sanitizer("requires deep stack", ub=True, thread=True)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:func:`annotationlib.get_annotations` now uses the ``__annotate__``
2+
attribute if it is present, even if ``__annotations__`` is not present.
3+
Additionally, the function now raises a :py:exc:`TypeError` if it is passed
4+
an object that does not have any annotatins.

0 commit comments

Comments
 (0)