mirror of
https://github.com/python/cpython.git
synced 2026-01-06 15:32:22 +00:00
gh-132064: Make annotationlib use __annotate__ if only it is present (#132195)
This commit is contained in:
parent
a1cd4ca7f4
commit
4d3ad0467e
5 changed files with 126 additions and 22 deletions
|
|
@ -317,11 +317,22 @@ Functions
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -885,6 +885,50 @@ def __annotate__(self):
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import abc
|
||||
from annotationlib import Format, get_annotations
|
||||
import builtins
|
||||
import collections
|
||||
import collections.abc
|
||||
|
|
@ -22,6 +23,7 @@
|
|||
|
||||
from test.support import import_helper
|
||||
from test.support import threading_helper
|
||||
from test.support import EqualToForwardRef
|
||||
|
||||
import functools
|
||||
|
||||
|
|
@ -2075,6 +2077,34 @@ def orig(a, /, b, c=True): ...
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
: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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue