Skip to content

Commit c7fda9b

Browse files
[3.14] 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 (GH-138803) (#140426)
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 (GH-138803) (cherry picked from commit 95c257e) Co-authored-by: David Ellis <[email protected]>
1 parent d9e3d0e commit c7fda9b

File tree

4 files changed

+209
-0
lines changed

4 files changed

+209
-0
lines changed

Doc/library/annotationlib.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,14 +340,29 @@ Functions
340340

341341
* VALUE: :attr:`!object.__annotations__` is tried first; if that does not exist,
342342
the :attr:`!object.__annotate__` function is called if it exists.
343+
343344
* FORWARDREF: If :attr:`!object.__annotations__` exists and can be evaluated successfully,
344345
it is used; otherwise, the :attr:`!object.__annotate__` function is called. If it
345346
does not exist either, :attr:`!object.__annotations__` is tried again and any error
346347
from accessing it is re-raised.
348+
349+
* When calling :attr:`!object.__annotate__` it is first called with :attr:`~Format.FORWARDREF`.
350+
If this is not implemented, it will then check if :attr:`~Format.VALUE_WITH_FAKE_GLOBALS`
351+
is supported and use that in the fake globals environment.
352+
If neither of these formats are supported, it will fall back to using :attr:`~Format.VALUE`.
353+
If :attr:`~Format.VALUE` fails, the error from this call will be raised.
354+
347355
* STRING: If :attr:`!object.__annotate__` exists, it is called first;
348356
otherwise, :attr:`!object.__annotations__` is used and stringified
349357
using :func:`annotations_to_string`.
350358

359+
* When calling :attr:`!object.__annotate__` it is first called with :attr:`~Format.STRING`.
360+
If this is not implemented, it will then check if :attr:`~Format.VALUE_WITH_FAKE_GLOBALS`
361+
is supported and use that in the fake globals environment.
362+
If neither of these formats are supported, it will fall back to using :attr:`~Format.VALUE`
363+
with the result converted using :func:`annotations_to_string`.
364+
If :attr:`~Format.VALUE` fails, the error from this call will be raised.
365+
351366
Returns a dict. :func:`!get_annotations` returns a new dict every time
352367
it's called; calling it twice on the same object will return two
353368
different but equivalent dicts.

Lib/annotationlib.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,18 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
695695
# possibly constants if the annotate function uses them directly). We then
696696
# convert each of those into a string to get an approximation of the
697697
# original source.
698+
699+
# Attempt to call with VALUE_WITH_FAKE_GLOBALS to check if it is implemented
700+
# See: https://github.com/python/cpython/issues/138764
701+
# Only fail on NotImplementedError
702+
try:
703+
annotate(Format.VALUE_WITH_FAKE_GLOBALS)
704+
except NotImplementedError:
705+
# Both STRING and VALUE_WITH_FAKE_GLOBALS are not implemented: fallback to VALUE
706+
return annotations_to_string(annotate(Format.VALUE))
707+
except Exception:
708+
pass
709+
698710
globals = _StringifierDict({}, format=format)
699711
is_class = isinstance(owner, type)
700712
closure = _build_closure(
@@ -753,6 +765,9 @@ def call_annotate_function(annotate, format, *, owner=None, _is_evaluate=False):
753765
)
754766
try:
755767
result = func(Format.VALUE_WITH_FAKE_GLOBALS)
768+
except NotImplementedError:
769+
# FORWARDREF and VALUE_WITH_FAKE_GLOBALS not supported, fall back to VALUE
770+
return annotate(Format.VALUE)
756771
except Exception:
757772
pass
758773
else:

Lib/test/test_annotationlib.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,6 +1194,25 @@ class RaisesAttributeError:
11941194
},
11951195
)
11961196

1197+
def test_raises_error_from_value(self):
1198+
# test that if VALUE is the only supported format, but raises an error
1199+
# that error is propagated from get_annotations
1200+
class DemoException(Exception): ...
1201+
1202+
def annotate(format, /):
1203+
if format == Format.VALUE:
1204+
raise DemoException()
1205+
else:
1206+
raise NotImplementedError(format)
1207+
1208+
def f(): ...
1209+
1210+
f.__annotate__ = annotate
1211+
1212+
for fmt in [Format.VALUE, Format.FORWARDREF, Format.STRING]:
1213+
with self.assertRaises(DemoException):
1214+
get_annotations(f, format=fmt)
1215+
11971216

11981217
class TestCallEvaluateFunction(unittest.TestCase):
11991218
def test_evaluation(self):
@@ -1214,6 +1233,163 @@ def evaluate(format, exc=NotImplementedError):
12141233
)
12151234

12161235

1236+
class TestCallAnnotateFunction(unittest.TestCase):
1237+
# Tests for user defined annotate functions.
1238+
1239+
# Format and NotImplementedError are provided as arguments so they exist in
1240+
# the fake globals namespace.
1241+
# This avoids non-matching conditions passing by being converted to stringifiers.
1242+
# See: https://github.com/python/cpython/issues/138764
1243+
1244+
def test_user_annotate_value(self):
1245+
def annotate(format, /):
1246+
if format == Format.VALUE:
1247+
return {"x": str}
1248+
else:
1249+
raise NotImplementedError(format)
1250+
1251+
annotations = annotationlib.call_annotate_function(
1252+
annotate,
1253+
Format.VALUE,
1254+
)
1255+
1256+
self.assertEqual(annotations, {"x": str})
1257+
1258+
def test_user_annotate_forwardref_supported(self):
1259+
# If Format.FORWARDREF is supported prefer it over Format.VALUE
1260+
def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError):
1261+
if format == __Format.VALUE:
1262+
return {'x': str}
1263+
elif format == __Format.VALUE_WITH_FAKE_GLOBALS:
1264+
return {'x': int}
1265+
elif format == __Format.FORWARDREF:
1266+
return {'x': float}
1267+
else:
1268+
raise __NotImplementedError(format)
1269+
1270+
annotations = annotationlib.call_annotate_function(
1271+
annotate,
1272+
Format.FORWARDREF
1273+
)
1274+
1275+
self.assertEqual(annotations, {"x": float})
1276+
1277+
def test_user_annotate_forwardref_fakeglobals(self):
1278+
# If Format.FORWARDREF is not supported, use Format.VALUE_WITH_FAKE_GLOBALS
1279+
# before falling back to Format.VALUE
1280+
def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError):
1281+
if format == __Format.VALUE:
1282+
return {'x': str}
1283+
elif format == __Format.VALUE_WITH_FAKE_GLOBALS:
1284+
return {'x': int}
1285+
else:
1286+
raise __NotImplementedError(format)
1287+
1288+
annotations = annotationlib.call_annotate_function(
1289+
annotate,
1290+
Format.FORWARDREF
1291+
)
1292+
1293+
self.assertEqual(annotations, {"x": int})
1294+
1295+
def test_user_annotate_forwardref_value_fallback(self):
1296+
# If Format.FORWARDREF and Format.VALUE_WITH_FAKE_GLOBALS are not supported
1297+
# use Format.VALUE
1298+
def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError):
1299+
if format == __Format.VALUE:
1300+
return {"x": str}
1301+
else:
1302+
raise __NotImplementedError(format)
1303+
1304+
annotations = annotationlib.call_annotate_function(
1305+
annotate,
1306+
Format.FORWARDREF,
1307+
)
1308+
1309+
self.assertEqual(annotations, {"x": str})
1310+
1311+
def test_user_annotate_string_supported(self):
1312+
# If Format.STRING is supported prefer it over Format.VALUE
1313+
def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError):
1314+
if format == __Format.VALUE:
1315+
return {'x': str}
1316+
elif format == __Format.VALUE_WITH_FAKE_GLOBALS:
1317+
return {'x': int}
1318+
elif format == __Format.STRING:
1319+
return {'x': "float"}
1320+
else:
1321+
raise __NotImplementedError(format)
1322+
1323+
annotations = annotationlib.call_annotate_function(
1324+
annotate,
1325+
Format.STRING,
1326+
)
1327+
1328+
self.assertEqual(annotations, {"x": "float"})
1329+
1330+
def test_user_annotate_string_fakeglobals(self):
1331+
# If Format.STRING is not supported but Format.VALUE_WITH_FAKE_GLOBALS is
1332+
# prefer that over Format.VALUE
1333+
def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError):
1334+
if format == __Format.VALUE:
1335+
return {'x': str}
1336+
elif format == __Format.VALUE_WITH_FAKE_GLOBALS:
1337+
return {'x': int}
1338+
else:
1339+
raise __NotImplementedError(format)
1340+
1341+
annotations = annotationlib.call_annotate_function(
1342+
annotate,
1343+
Format.STRING,
1344+
)
1345+
1346+
self.assertEqual(annotations, {"x": "int"})
1347+
1348+
def test_user_annotate_string_value_fallback(self):
1349+
# If Format.STRING and Format.VALUE_WITH_FAKE_GLOBALS are not
1350+
# supported fall back to Format.VALUE and convert to strings
1351+
def annotate(format, /, __Format=Format, __NotImplementedError=NotImplementedError):
1352+
if format == __Format.VALUE:
1353+
return {"x": str}
1354+
else:
1355+
raise __NotImplementedError(format)
1356+
1357+
annotations = annotationlib.call_annotate_function(
1358+
annotate,
1359+
Format.STRING,
1360+
)
1361+
1362+
self.assertEqual(annotations, {"x": "str"})
1363+
1364+
def test_condition_not_stringified(self):
1365+
# Make sure the first condition isn't evaluated as True by being converted
1366+
# to a _Stringifier
1367+
def annotate(format, /):
1368+
if format == Format.FORWARDREF:
1369+
return {"x": str}
1370+
else:
1371+
raise NotImplementedError(format)
1372+
1373+
with self.assertRaises(NotImplementedError):
1374+
annotationlib.call_annotate_function(annotate, Format.STRING)
1375+
1376+
def test_error_from_value_raised(self):
1377+
# Test that the error from format.VALUE is raised
1378+
# if all formats fail
1379+
1380+
class DemoException(Exception): ...
1381+
1382+
def annotate(format, /):
1383+
if format == Format.VALUE:
1384+
raise DemoException()
1385+
else:
1386+
raise NotImplementedError(format)
1387+
1388+
for fmt in [Format.VALUE, Format.FORWARDREF, Format.STRING]:
1389+
with self.assertRaises(DemoException):
1390+
annotationlib.call_annotate_function(annotate, format=fmt)
1391+
1392+
12171393
class MetaclassTests(unittest.TestCase):
12181394
def test_annotated_meta(self):
12191395
class Meta(type):
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
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.
2+
3+
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.

0 commit comments

Comments
 (0)