Skip to content

Commit f77be37

Browse files
committed
Merge remote-tracking branch 'upstream/main' into pep649-fakevalue
2 parents 2012bb4 + 4e829c0 commit f77be37

File tree

5 files changed

+113
-42
lines changed

5 files changed

+113
-42
lines changed

Doc/library/annotationlib.rst

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,27 @@ Classes
208208
Functions
209209
---------
210210

211+
.. function:: annotations_to_source(annotations)
212+
213+
Convert an annotations dict containing runtime values to a
214+
dict containing only strings. If the values are not already strings,
215+
they are converted using :func:`value_to_source`.
216+
This is meant as a helper for user-provided
217+
annotate functions that support the :attr:`~Format.SOURCE` format but
218+
do not have access to the code creating the annotations.
219+
220+
For example, this is used to implement the :attr:`~Format.SOURCE` for
221+
:class:`typing.TypedDict` classes created through the functional syntax:
222+
223+
.. doctest::
224+
225+
>>> from typing import TypedDict
226+
>>> Movie = TypedDict("movie", {"name": str, "year": int})
227+
>>> get_annotations(Movie, format=Format.SOURCE)
228+
{'name': 'str', 'year': 'int'}
229+
230+
.. versionadded:: 3.14
231+
211232
.. function:: call_annotate_function(annotate, format, *, owner=None)
212233

213234
Call the :term:`annotate function` *annotate* with the given *format*,
@@ -358,3 +379,18 @@ Functions
358379
{'a': <class 'int'>, 'b': <class 'str'>, 'return': <class 'float'>}
359380

360381
.. versionadded:: 3.14
382+
383+
.. function:: value_to_source(value)
384+
385+
Convert an arbitrary Python value to a format suitable for use by the
386+
:attr:`~Format.SOURCE` format. This calls :func:`repr` for most
387+
objects, but has special handling for some objects, such as type objects.
388+
389+
This is meant as a helper for user-provided
390+
annotate functions that support the :attr:`~Format.SOURCE` format but
391+
do not have access to the code creating the annotations. It can also
392+
be used to provide a user-friendly string representation for other
393+
objects that contain values that are commonly encountered in annotations.
394+
395+
.. versionadded:: 3.14
396+

Lib/_collections_abc.py

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -485,9 +485,10 @@ def __new__(cls, origin, args):
485485
def __repr__(self):
486486
if len(self.__args__) == 2 and _is_param_expr(self.__args__[0]):
487487
return super().__repr__()
488+
from annotationlib import value_to_source
488489
return (f'collections.abc.Callable'
489-
f'[[{", ".join([_type_repr(a) for a in self.__args__[:-1]])}], '
490-
f'{_type_repr(self.__args__[-1])}]')
490+
f'[[{", ".join([value_to_source(a) for a in self.__args__[:-1]])}], '
491+
f'{value_to_source(self.__args__[-1])}]')
491492

492493
def __reduce__(self):
493494
args = self.__args__
@@ -524,23 +525,6 @@ def _is_param_expr(obj):
524525
names = ('ParamSpec', '_ConcatenateGenericAlias')
525526
return obj.__module__ == 'typing' and any(obj.__name__ == name for name in names)
526527

527-
def _type_repr(obj):
528-
"""Return the repr() of an object, special-casing types (internal helper).
529-
530-
Copied from :mod:`typing` since collections.abc
531-
shouldn't depend on that module.
532-
(Keep this roughly in sync with the typing version.)
533-
"""
534-
if isinstance(obj, type):
535-
if obj.__module__ == 'builtins':
536-
return obj.__qualname__
537-
return f'{obj.__module__}.{obj.__qualname__}'
538-
if obj is Ellipsis:
539-
return '...'
540-
if isinstance(obj, FunctionType):
541-
return obj.__name__
542-
return repr(obj)
543-
544528

545529
class Callable(metaclass=ABCMeta):
546530

Lib/annotationlib.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
"call_evaluate_function",
1616
"get_annotate_function",
1717
"get_annotations",
18+
"annotations_to_source",
19+
"value_to_source",
1820
]
1921

2022

@@ -696,7 +698,7 @@ def get_annotations(
696698
return ann
697699
# But if we didn't get it, we use __annotations__ instead.
698700
ann = _get_dunder_annotations(obj)
699-
return ann
701+
return annotations_to_source(ann)
700702
case Format.VALUE_WITH_FAKE_GLOBALS:
701703
raise ValueError("The VALUE_WITH_FAKE_GLOBALS format is for internal use only")
702704
case _:
@@ -767,6 +769,33 @@ def get_annotations(
767769
return return_value
768770

769771

772+
def value_to_source(value):
773+
"""Convert a Python value to a format suitable for use with the SOURCE format.
774+
775+
This is inteded as a helper for tools that support the SOURCE format but do
776+
not have access to the code that originally produced the annotations. It uses
777+
repr() for most objects.
778+
779+
"""
780+
if isinstance(value, type):
781+
if value.__module__ == "builtins":
782+
return value.__qualname__
783+
return f"{value.__module__}.{value.__qualname__}"
784+
if value is ...:
785+
return "..."
786+
if isinstance(value, (types.FunctionType, types.BuiltinFunctionType)):
787+
return value.__name__
788+
return repr(value)
789+
790+
791+
def annotations_to_source(annotations):
792+
"""Convert an annotation dict containing values to approximately the SOURCE format."""
793+
return {
794+
n: t if isinstance(t, str) else value_to_source(t)
795+
for n, t in annotations.items()
796+
}
797+
798+
770799
def _get_and_call_annotate(obj, format):
771800
annotate = get_annotate_function(obj)
772801
if annotate is not None:

Lib/test/test_annotationlib.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@
77
import itertools
88
import pickle
99
import unittest
10-
from annotationlib import Format, ForwardRef, get_annotations, get_annotate_function
10+
from annotationlib import (
11+
Format,
12+
ForwardRef,
13+
get_annotations,
14+
get_annotate_function,
15+
annotations_to_source,
16+
value_to_source,
17+
)
1118
from typing import Unpack
1219

1320
from test import support
@@ -25,6 +32,11 @@ def wrapper(a, b):
2532
return wrapper
2633

2734

35+
class MyClass:
36+
def __repr__(self):
37+
return "my repr"
38+
39+
2840
class TestFormat(unittest.TestCase):
2941
def test_enum(self):
3042
self.assertEqual(annotationlib.Format.VALUE.value, 1)
@@ -327,7 +339,10 @@ def test_name_lookup_without_eval(self):
327339
# namespaces without going through eval()
328340
self.assertIs(ForwardRef("int").evaluate(), int)
329341
self.assertIs(ForwardRef("int").evaluate(locals={"int": str}), str)
330-
self.assertIs(ForwardRef("int").evaluate(locals={"int": float}, globals={"int": str}), float)
342+
self.assertIs(
343+
ForwardRef("int").evaluate(locals={"int": float}, globals={"int": str}),
344+
float,
345+
)
331346
self.assertIs(ForwardRef("int").evaluate(globals={"int": str}), str)
332347
with support.swap_attr(builtins, "int", dict):
333348
self.assertIs(ForwardRef("int").evaluate(), dict)
@@ -804,9 +819,8 @@ def __annotations__(self):
804819
annotationlib.get_annotations(ha, format=Format.FORWARDREF), {"x": int}
805820
)
806821

807-
# TODO(gh-124412): This should return {'x': 'int'} instead.
808822
self.assertEqual(
809-
annotationlib.get_annotations(ha, format=Format.SOURCE), {"x": int}
823+
annotationlib.get_annotations(ha, format=Format.SOURCE), {"x": "int"}
810824
)
811825

812826
def test_raising_annotations_on_custom_object(self):
@@ -1094,6 +1108,29 @@ class C:
10941108
self.assertEqual(get_annotate_function(C)(Format.VALUE), {"a": int})
10951109

10961110

1111+
class TestToSource(unittest.TestCase):
1112+
def test_value_to_source(self):
1113+
self.assertEqual(value_to_source(int), "int")
1114+
self.assertEqual(value_to_source(MyClass), "test.test_annotationlib.MyClass")
1115+
self.assertEqual(value_to_source(len), "len")
1116+
self.assertEqual(value_to_source(value_to_source), "value_to_source")
1117+
self.assertEqual(value_to_source(times_three), "times_three")
1118+
self.assertEqual(value_to_source(...), "...")
1119+
self.assertEqual(value_to_source(None), "None")
1120+
self.assertEqual(value_to_source(1), "1")
1121+
self.assertEqual(value_to_source("1"), "'1'")
1122+
self.assertEqual(value_to_source(Format.VALUE), repr(Format.VALUE))
1123+
self.assertEqual(value_to_source(MyClass()), "my repr")
1124+
1125+
def test_annotations_to_source(self):
1126+
self.assertEqual(annotations_to_source({}), {})
1127+
self.assertEqual(annotations_to_source({"x": int}), {"x": "int"})
1128+
self.assertEqual(annotations_to_source({"x": "int"}), {"x": "int"})
1129+
self.assertEqual(
1130+
annotations_to_source({"x": int, "y": str}), {"x": "int", "y": "str"}
1131+
)
1132+
1133+
10971134
class TestAnnotationLib(unittest.TestCase):
10981135
def test__all__(self):
10991136
support.check__all__(self, annotationlib)

Lib/typing.py

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -242,21 +242,10 @@ def _type_repr(obj):
242242
typically enough to uniquely identify a type. For everything
243243
else, we fall back on repr(obj).
244244
"""
245-
# When changing this function, don't forget about
246-
# `_collections_abc._type_repr`, which does the same thing
247-
# and must be consistent with this one.
248-
if isinstance(obj, type):
249-
if obj.__module__ == 'builtins':
250-
return obj.__qualname__
251-
return f'{obj.__module__}.{obj.__qualname__}'
252-
if obj is ...:
253-
return '...'
254-
if isinstance(obj, types.FunctionType):
255-
return obj.__name__
256245
if isinstance(obj, tuple):
257246
# Special case for `repr` of types with `ParamSpec`:
258247
return '[' + ', '.join(_type_repr(t) for t in obj) + ']'
259-
return repr(obj)
248+
return annotationlib.value_to_source(obj)
260249

261250

262251
def _collect_type_parameters(args, *, enforce_default_ordering: bool = True):
@@ -2949,16 +2938,12 @@ def annotate(format):
29492938
case annotationlib.Format.VALUE | annotationlib.Format.FORWARDREF:
29502939
return checked_types
29512940
case annotationlib.Format.SOURCE:
2952-
return _convert_to_source(types)
2941+
return annotationlib.annotations_to_source(types)
29532942
case _:
29542943
raise NotImplementedError(format)
29552944
return annotate
29562945

29572946

2958-
def _convert_to_source(types):
2959-
return {n: t if isinstance(t, str) else _type_repr(t) for n, t in types.items()}
2960-
2961-
29622947
# attributes prohibited to set in NamedTuple class syntax
29632948
_prohibited = frozenset({'__new__', '__init__', '__slots__', '__getnewargs__',
29642949
'_fields', '_field_defaults',
@@ -3244,7 +3229,7 @@ def __annotate__(format):
32443229
for n, tp in own.items()
32453230
}
32463231
elif format == annotationlib.Format.SOURCE:
3247-
own = _convert_to_source(own_annotations)
3232+
own = annotationlib.annotations_to_source(own_annotations)
32483233
elif format in (annotationlib.Format.FORWARDREF, annotationlib.Format.VALUE):
32493234
own = own_checked_annotations
32503235
else:

0 commit comments

Comments
 (0)