Compute the annotations dict for an object.
*obj* may be a callable, class, module, or other object with
- :attr:`~object.__annotate__` and :attr:`~object.__annotations__` attributes.
- Passing in an object of any other type raises :exc:`TypeError`.
+ :attr:`~object.__annotate__` or :attr:`~object.__annotations__` attributes.
+ Passing any other object raises :exc:`TypeError`.
The *format* parameter controls the format in which annotations are returned,
and must be a member of the :class:`Format` enum or its integer equivalent.
+ The different formats work as follows:
+
+ * VALUE: :attr:`!object.__annotations__` is tried first; if that does not exist,
+ the :attr:`!object.__annotate__` function is called if it exists.
+ * FORWARDREF: If :attr:`!object.__annotations__` exists and can be evaluated successfully,
+ it is used; otherwise, the :attr:`!object.__annotate__` function is called. If it
+ does not exist either, :attr:`!object.__annotations__` is tried again and any error
+ from accessing it is re-raised.
+ * STRING: If :attr:`!object.__annotate__` exists, it is called first;
+ otherwise, :attr:`!object.__annotations__` is used and stringified
+ using :func:`annotations_to_string`.
Returns a dict. :func:`!get_annotations` returns a new dict every time
it's called; calling it twice on the same object will return two
):
"""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.
-
- 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.
+ obj may be a callable, class, module, or other object with
+ __annotate__ or __annotations__ attributes.
+ Passing any other object raises TypeError.
+
+ 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:
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)
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 {}
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:
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")
annotationlib.get_annotations(hb, format=Format.STRING), {"x": str}
)
+ def test_only_annotate(self):
+ def f(x: int):
+ pass
+
+ class OnlyAnnotate:
+ @property
+ def __annotate__(self):
+ return f.__annotate__
+
+ oa = OnlyAnnotate()
+ self.assertEqual(
+ annotationlib.get_annotations(oa, format=Format.VALUE), {"x": int}
+ )
+ self.assertEqual(
+ annotationlib.get_annotations(oa, format=Format.FORWARDREF), {"x": int}
+ )
+ self.assertEqual(
+ annotationlib.get_annotations(oa, format=Format.STRING),
+ {"x": "int"},
+ )
+
+ def test_no_annotations(self):
+ class CustomClass:
+ pass
+
+ class MyCallable:
+ def __call__(self):
+ pass
+
+ for format in Format:
+ if format == Format.VALUE_WITH_FAKE_GLOBALS:
+ continue
+ for obj in (None, 1, object(), CustomClass()):
+ with self.subTest(format=format, obj=obj):
+ with self.assertRaises(TypeError):
+ annotationlib.get_annotations(obj, format=format)
+
+ # Callables and types with no annotations return an empty dict
+ for obj in (int, len, MyCallable()):
+ with self.subTest(format=format, obj=obj):
+ self.assertEqual(
+ annotationlib.get_annotations(obj, format=format), {}
+ )
+
def test_pep695_generic_class_with_future_annotations(self):
ann_module695 = inspect_stringized_annotations_pep695
A_annotations = annotationlib.get_annotations(ann_module695.A, eval_str=True)
import abc
+from annotationlib import Format, get_annotations
import builtins
import collections
import collections.abc
from test.support import import_helper
from test.support import threading_helper
+from test.support import EqualToForwardRef
import functools
self.assertEqual(str(Signature.from_callable(lru.cache_info)), '()')
self.assertEqual(str(Signature.from_callable(lru.cache_clear)), '()')
+ def test_get_annotations(self):
+ def orig(a: int) -> str: ...
+ lru = self.module.lru_cache(1)(orig)
+
+ self.assertEqual(
+ get_annotations(orig), {"a": int, "return": str},
+ )
+ self.assertEqual(
+ get_annotations(lru), {"a": int, "return": str},
+ )
+
+ def test_get_annotations_with_forwardref(self):
+ def orig(a: int) -> nonexistent: ...
+ lru = self.module.lru_cache(1)(orig)
+
+ self.assertEqual(
+ get_annotations(orig, format=Format.FORWARDREF),
+ {"a": int, "return": EqualToForwardRef('nonexistent', owner=orig)},
+ )
+ self.assertEqual(
+ get_annotations(lru, format=Format.FORWARDREF),
+ {"a": int, "return": EqualToForwardRef('nonexistent', owner=lru)},
+ )
+ with self.assertRaises(NameError):
+ get_annotations(orig, format=Format.VALUE)
+ with self.assertRaises(NameError):
+ get_annotations(lru, format=Format.VALUE)
+
@support.skip_on_s390x
@unittest.skipIf(support.is_wasi, "WASI has limited C stack")
@support.skip_if_sanitizer("requires deep stack", ub=True, thread=True)
--- /dev/null
+:func:`annotationlib.get_annotations` now uses the ``__annotate__``
+attribute if it is present, even if ``__annotations__`` is not present.
+Additionally, the function now raises a :py:exc:`TypeError` if it is passed
+an object that does not have any annotatins.