mirror of
https://github.com/python/cpython.git
synced 2025-12-08 06:10:17 +00:00
gh-138764: annotationlib: Make call_annotate_function fallback to using VALUE annotations if both the requested format and VALUE_WITH_FAKE_GLOBALS are not implemented (#138803)
This commit is contained in:
parent
c788bfb80e
commit
95c257e2e6
4 changed files with 209 additions and 0 deletions
|
|
@ -340,14 +340,29 @@ Functions
|
||||||
|
|
||||||
* VALUE: :attr:`!object.__annotations__` is tried first; if that does not exist,
|
* VALUE: :attr:`!object.__annotations__` is tried first; if that does not exist,
|
||||||
the :attr:`!object.__annotate__` function is called if it exists.
|
the :attr:`!object.__annotate__` function is called if it exists.
|
||||||
|
|
||||||
* FORWARDREF: If :attr:`!object.__annotations__` exists and can be evaluated successfully,
|
* FORWARDREF: If :attr:`!object.__annotations__` exists and can be evaluated successfully,
|
||||||
it is used; otherwise, the :attr:`!object.__annotate__` function is called. If it
|
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
|
does not exist either, :attr:`!object.__annotations__` is tried again and any error
|
||||||
from accessing it is re-raised.
|
from accessing it is re-raised.
|
||||||
|
|
||||||
|
* When calling :attr:`!object.__annotate__` it is first called with :attr:`~Format.FORWARDREF`.
|
||||||
|
If this is not implemented, it will then check if :attr:`~Format.VALUE_WITH_FAKE_GLOBALS`
|
||||||
|
is supported and use that in the fake globals environment.
|
||||||
|
If neither of these formats are supported, it will fall back to using :attr:`~Format.VALUE`.
|
||||||
|
If :attr:`~Format.VALUE` fails, the error from this call will be raised.
|
||||||
|
|
||||||
* STRING: If :attr:`!object.__annotate__` exists, it is called first;
|
* STRING: If :attr:`!object.__annotate__` exists, it is called first;
|
||||||
otherwise, :attr:`!object.__annotations__` is used and stringified
|
otherwise, :attr:`!object.__annotations__` is used and stringified
|
||||||
using :func:`annotations_to_string`.
|
using :func:`annotations_to_string`.
|
||||||
|
|
||||||
|
* When calling :attr:`!object.__annotate__` it is first called with :attr:`~Format.STRING`.
|
||||||
|
If this is not implemented, it will then check if :attr:`~Format.VALUE_WITH_FAKE_GLOBALS`
|
||||||
|
is supported and use that in the fake globals environment.
|
||||||
|
If neither of these formats are supported, it will fall back to using :attr:`~Format.VALUE`
|
||||||
|
with the result converted using :func:`annotations_to_string`.
|
||||||
|
If :attr:`~Format.VALUE` fails, the error from this call will be raised.
|
||||||
|
|
||||||
Returns a dict. :func:`!get_annotations` returns a new dict every time
|
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
|
it's called; calling it twice on the same object will return two
|
||||||
different but equivalent dicts.
|
different but equivalent dicts.
|
||||||
|
|
|
||||||
|
|
@ -695,6 +695,18 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
|
||||||
# possibly constants if the annotate function uses them directly). We then
|
# possibly constants if the annotate function uses them directly). We then
|
||||||
# convert each of those into a string to get an approximation of the
|
# convert each of those into a string to get an approximation of the
|
||||||
# original source.
|
# original source.
|
||||||
|
|
||||||
|
# Attempt to call with VALUE_WITH_FAKE_GLOBALS to check if it is implemented
|
||||||
|
# See: https://github.com/python/cpython/issues/138764
|
||||||
|
# Only fail on NotImplementedError
|
||||||
|
try:
|
||||||
|
annotate(Format.VALUE_WITH_FAKE_GLOBALS)
|
||||||
|
except NotImplementedError:
|
||||||
|
# Both STRING and VALUE_WITH_FAKE_GLOBALS are not implemented: fallback to VALUE
|
||||||
|
return annotations_to_string(annotate(Format.VALUE))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
globals = _StringifierDict({}, format=format)
|
globals = _StringifierDict({}, format=format)
|
||||||
is_class = isinstance(owner, type)
|
is_class = isinstance(owner, type)
|
||||||
closure = _build_closure(
|
closure = _build_closure(
|
||||||
|
|
@ -753,6 +765,9 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
result = func(Format.VALUE_WITH_FAKE_GLOBALS)
|
result = func(Format.VALUE_WITH_FAKE_GLOBALS)
|
||||||
|
except NotImplementedError:
|
||||||
|
# FORWARDREF and VALUE_WITH_FAKE_GLOBALS not supported, fall back to VALUE
|
||||||
|
return annotate(Format.VALUE)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -1194,6 +1194,25 @@ class RaisesAttributeError:
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_raises_error_from_value(self):
|
||||||
|
# test that if VALUE is the only supported format, but raises an error
|
||||||
|
# that error is propagated from get_annotations
|
||||||
|
class DemoException(Exception): ...
|
||||||
|
|
||||||
|
def annotate(format, /):
|
||||||
|
if format == Format.VALUE:
|
||||||
|
raise DemoException()
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(format)
|
||||||
|
|
||||||
|
def f(): ...
|
||||||
|
|
||||||
|
f.__annotate__ = annotate
|
||||||
|
|
||||||
|
for fmt in [Format.VALUE, Format.FORWARDREF, Format.STRING]:
|
||||||
|
with self.assertRaises(DemoException):
|
||||||
|
get_annotations(f, format=fmt)
|
||||||
|
|
||||||
|
|
||||||
class TestCallEvaluateFunction(unittest.TestCase):
|
class TestCallEvaluateFunction(unittest.TestCase):
|
||||||
def test_evaluation(self):
|
def test_evaluation(self):
|
||||||
|
|
@ -1214,6 +1233,163 @@ def evaluate(format, exc=NotImplementedError):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestCallAnnotateFunction(unittest.TestCase):
|
||||||
|
# Tests for user defined annotate functions.
|
||||||
|
|
||||||
|
# Format and NotImplementedError are provided as arguments so they exist in
|
||||||
|
# the fake globals namespace.
|
||||||
|
# This avoids non-matching conditions passing by being converted to stringifiers.
|
||||||
|
# See: https://github.com/python/cpython/issues/138764
|
||||||
|
|
||||||
|
def test_user_annotate_value(self):
|
||||||
|
def annotate(format, /):
|
||||||
|
if format == Format.VALUE:
|
||||||
|
return {"x": str}
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(format)
|
||||||
|
|
||||||
|
annotations = annotationlib.call_annotate_function(
|
||||||
|
annotate,
|
||||||
|
Format.VALUE,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(annotations, {"x": str})
|
||||||
|
|
||||||
|
def test_user_annotate_forwardref_supported(self):
|
||||||
|
# If Format.FORWARDREF is supported prefer it over Format.VALUE
|
||||||
|
def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError):
|
||||||
|
if format == __Format.VALUE:
|
||||||
|
return {'x': str}
|
||||||
|
elif format == __Format.VALUE_WITH_FAKE_GLOBALS:
|
||||||
|
return {'x': int}
|
||||||
|
elif format == __Format.FORWARDREF:
|
||||||
|
return {'x': float}
|
||||||
|
else:
|
||||||
|
raise __NotImplementedError(format)
|
||||||
|
|
||||||
|
annotations = annotationlib.call_annotate_function(
|
||||||
|
annotate,
|
||||||
|
Format.FORWARDREF
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(annotations, {"x": float})
|
||||||
|
|
||||||
|
def test_user_annotate_forwardref_fakeglobals(self):
|
||||||
|
# If Format.FORWARDREF is not supported, use Format.VALUE_WITH_FAKE_GLOBALS
|
||||||
|
# before falling back to Format.VALUE
|
||||||
|
def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError):
|
||||||
|
if format == __Format.VALUE:
|
||||||
|
return {'x': str}
|
||||||
|
elif format == __Format.VALUE_WITH_FAKE_GLOBALS:
|
||||||
|
return {'x': int}
|
||||||
|
else:
|
||||||
|
raise __NotImplementedError(format)
|
||||||
|
|
||||||
|
annotations = annotationlib.call_annotate_function(
|
||||||
|
annotate,
|
||||||
|
Format.FORWARDREF
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(annotations, {"x": int})
|
||||||
|
|
||||||
|
def test_user_annotate_forwardref_value_fallback(self):
|
||||||
|
# If Format.FORWARDREF and Format.VALUE_WITH_FAKE_GLOBALS are not supported
|
||||||
|
# use Format.VALUE
|
||||||
|
def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError):
|
||||||
|
if format == __Format.VALUE:
|
||||||
|
return {"x": str}
|
||||||
|
else:
|
||||||
|
raise __NotImplementedError(format)
|
||||||
|
|
||||||
|
annotations = annotationlib.call_annotate_function(
|
||||||
|
annotate,
|
||||||
|
Format.FORWARDREF,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(annotations, {"x": str})
|
||||||
|
|
||||||
|
def test_user_annotate_string_supported(self):
|
||||||
|
# If Format.STRING is supported prefer it over Format.VALUE
|
||||||
|
def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError):
|
||||||
|
if format == __Format.VALUE:
|
||||||
|
return {'x': str}
|
||||||
|
elif format == __Format.VALUE_WITH_FAKE_GLOBALS:
|
||||||
|
return {'x': int}
|
||||||
|
elif format == __Format.STRING:
|
||||||
|
return {'x': "float"}
|
||||||
|
else:
|
||||||
|
raise __NotImplementedError(format)
|
||||||
|
|
||||||
|
annotations = annotationlib.call_annotate_function(
|
||||||
|
annotate,
|
||||||
|
Format.STRING,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(annotations, {"x": "float"})
|
||||||
|
|
||||||
|
def test_user_annotate_string_fakeglobals(self):
|
||||||
|
# If Format.STRING is not supported but Format.VALUE_WITH_FAKE_GLOBALS is
|
||||||
|
# prefer that over Format.VALUE
|
||||||
|
def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError):
|
||||||
|
if format == __Format.VALUE:
|
||||||
|
return {'x': str}
|
||||||
|
elif format == __Format.VALUE_WITH_FAKE_GLOBALS:
|
||||||
|
return {'x': int}
|
||||||
|
else:
|
||||||
|
raise __NotImplementedError(format)
|
||||||
|
|
||||||
|
annotations = annotationlib.call_annotate_function(
|
||||||
|
annotate,
|
||||||
|
Format.STRING,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(annotations, {"x": "int"})
|
||||||
|
|
||||||
|
def test_user_annotate_string_value_fallback(self):
|
||||||
|
# If Format.STRING and Format.VALUE_WITH_FAKE_GLOBALS are not
|
||||||
|
# supported fall back to Format.VALUE and convert to strings
|
||||||
|
def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError):
|
||||||
|
if format == __Format.VALUE:
|
||||||
|
return {"x": str}
|
||||||
|
else:
|
||||||
|
raise __NotImplementedError(format)
|
||||||
|
|
||||||
|
annotations = annotationlib.call_annotate_function(
|
||||||
|
annotate,
|
||||||
|
Format.STRING,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(annotations, {"x": "str"})
|
||||||
|
|
||||||
|
def test_condition_not_stringified(self):
|
||||||
|
# Make sure the first condition isn't evaluated as True by being converted
|
||||||
|
# to a _Stringifier
|
||||||
|
def annotate(format, /):
|
||||||
|
if format == Format.FORWARDREF:
|
||||||
|
return {"x": str}
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(format)
|
||||||
|
|
||||||
|
with self.assertRaises(NotImplementedError):
|
||||||
|
annotationlib.call_annotate_function(annotate, Format.STRING)
|
||||||
|
|
||||||
|
def test_error_from_value_raised(self):
|
||||||
|
# Test that the error from format.VALUE is raised
|
||||||
|
# if all formats fail
|
||||||
|
|
||||||
|
class DemoException(Exception): ...
|
||||||
|
|
||||||
|
def annotate(format, /):
|
||||||
|
if format == Format.VALUE:
|
||||||
|
raise DemoException()
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(format)
|
||||||
|
|
||||||
|
for fmt in [Format.VALUE, Format.FORWARDREF, Format.STRING]:
|
||||||
|
with self.assertRaises(DemoException):
|
||||||
|
annotationlib.call_annotate_function(annotate, format=fmt)
|
||||||
|
|
||||||
|
|
||||||
class MetaclassTests(unittest.TestCase):
|
class MetaclassTests(unittest.TestCase):
|
||||||
def test_annotated_meta(self):
|
def test_annotated_meta(self):
|
||||||
class Meta(type):
|
class Meta(type):
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
Prevent :func:`annotationlib.call_annotate_function` from calling ``__annotate__`` functions that don't support ``VALUE_WITH_FAKE_GLOBALS`` in a fake globals namespace with empty globals.
|
||||||
|
|
||||||
|
Make ``FORWARDREF`` and ``STRING`` annotations fall back to using ``VALUE`` annotations in the case that neither their own format, nor ``VALUE_WITH_FAKE_GLOBALS`` are supported.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue