diff options
author | Jelle Zijlstra <jelle.zijlstra@gmail.com> | 2024-09-26 00:01:09 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-09-26 00:01:09 (GMT) |
commit | 4e829c0e6fbd47818451660c234eacc42a0b4a08 (patch) | |
tree | 20f181a657ba4a3e65fe90c5971d04105d4bf9b5 /Lib | |
parent | 0268b072d84bc4be890d1b7459815ba1cb9f9945 (diff) | |
download | cpython-4e829c0e6fbd47818451660c234eacc42a0b4a08.zip cpython-4e829c0e6fbd47818451660c234eacc42a0b4a08.tar.gz cpython-4e829c0e6fbd47818451660c234eacc42a0b4a08.tar.bz2 |
gh-124412: Add helpers for converting annotations to source format (#124551)
Diffstat (limited to 'Lib')
-rw-r--r-- | Lib/_collections_abc.py | 22 | ||||
-rw-r--r-- | Lib/annotationlib.py | 31 | ||||
-rw-r--r-- | Lib/test/test_annotationlib.py | 45 | ||||
-rw-r--r-- | Lib/typing.py | 21 |
4 files changed, 77 insertions, 42 deletions
diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index 75252b3..4139cba 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -485,9 +485,10 @@ class _CallableGenericAlias(GenericAlias): def __repr__(self): if len(self.__args__) == 2 and _is_param_expr(self.__args__[0]): return super().__repr__() + from annotationlib import value_to_source return (f'collections.abc.Callable' - f'[[{", ".join([_type_repr(a) for a in self.__args__[:-1]])}], ' - f'{_type_repr(self.__args__[-1])}]') + f'[[{", ".join([value_to_source(a) for a in self.__args__[:-1]])}], ' + f'{value_to_source(self.__args__[-1])}]') def __reduce__(self): args = self.__args__ @@ -524,23 +525,6 @@ def _is_param_expr(obj): names = ('ParamSpec', '_ConcatenateGenericAlias') return obj.__module__ == 'typing' and any(obj.__name__ == name for name in names) -def _type_repr(obj): - """Return the repr() of an object, special-casing types (internal helper). - - Copied from :mod:`typing` since collections.abc - shouldn't depend on that module. - (Keep this roughly in sync with the typing version.) - """ - if isinstance(obj, type): - if obj.__module__ == 'builtins': - return obj.__qualname__ - return f'{obj.__module__}.{obj.__qualname__}' - if obj is Ellipsis: - return '...' - if isinstance(obj, FunctionType): - return obj.__name__ - return repr(obj) - class Callable(metaclass=ABCMeta): diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py index 20c9542..a027f4d 100644 --- a/Lib/annotationlib.py +++ b/Lib/annotationlib.py @@ -15,6 +15,8 @@ __all__ = [ "call_evaluate_function", "get_annotate_function", "get_annotations", + "annotations_to_source", + "value_to_source", ] @@ -693,7 +695,7 @@ def get_annotations( return ann # But if we didn't get it, we use __annotations__ instead. ann = _get_dunder_annotations(obj) - return ann + return annotations_to_source(ann) case _: raise ValueError(f"Unsupported format {format!r}") @@ -762,6 +764,33 @@ def get_annotations( return return_value +def value_to_source(value): + """Convert a Python value to a format suitable for use with the SOURCE format. + + This is inteded as a helper for tools that support the SOURCE format but do + not have access to the code that originally produced the annotations. It uses + repr() for most objects. + + """ + if isinstance(value, type): + if value.__module__ == "builtins": + return value.__qualname__ + return f"{value.__module__}.{value.__qualname__}" + if value is ...: + return "..." + if isinstance(value, (types.FunctionType, types.BuiltinFunctionType)): + return value.__name__ + return repr(value) + + +def annotations_to_source(annotations): + """Convert an annotation dict containing values to approximately the SOURCE format.""" + return { + n: t if isinstance(t, str) else value_to_source(t) + for n, t in annotations.items() + } + + def _get_and_call_annotate(obj, format): annotate = get_annotate_function(obj) if annotate is not None: diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py index 5b052da..dc1106a 100644 --- a/Lib/test/test_annotationlib.py +++ b/Lib/test/test_annotationlib.py @@ -7,7 +7,14 @@ import functools import itertools import pickle import unittest -from annotationlib import Format, ForwardRef, get_annotations, get_annotate_function +from annotationlib import ( + Format, + ForwardRef, + get_annotations, + get_annotate_function, + annotations_to_source, + value_to_source, +) from typing import Unpack from test import support @@ -25,6 +32,11 @@ def times_three(fn): return wrapper +class MyClass: + def __repr__(self): + return "my repr" + + class TestFormat(unittest.TestCase): def test_enum(self): self.assertEqual(annotationlib.Format.VALUE.value, 1) @@ -324,7 +336,10 @@ class TestForwardRefClass(unittest.TestCase): # namespaces without going through eval() self.assertIs(ForwardRef("int").evaluate(), int) self.assertIs(ForwardRef("int").evaluate(locals={"int": str}), str) - self.assertIs(ForwardRef("int").evaluate(locals={"int": float}, globals={"int": str}), float) + self.assertIs( + ForwardRef("int").evaluate(locals={"int": float}, globals={"int": str}), + float, + ) self.assertIs(ForwardRef("int").evaluate(globals={"int": str}), str) with support.swap_attr(builtins, "int", dict): self.assertIs(ForwardRef("int").evaluate(), dict) @@ -788,9 +803,8 @@ class TestGetAnnotations(unittest.TestCase): annotationlib.get_annotations(ha, format=Format.FORWARDREF), {"x": int} ) - # TODO(gh-124412): This should return {'x': 'int'} instead. self.assertEqual( - annotationlib.get_annotations(ha, format=Format.SOURCE), {"x": int} + annotationlib.get_annotations(ha, format=Format.SOURCE), {"x": "int"} ) def test_raising_annotations_on_custom_object(self): @@ -1078,6 +1092,29 @@ class TestGetAnnotateFunction(unittest.TestCase): self.assertEqual(get_annotate_function(C)(Format.VALUE), {"a": int}) +class TestToSource(unittest.TestCase): + def test_value_to_source(self): + self.assertEqual(value_to_source(int), "int") + self.assertEqual(value_to_source(MyClass), "test.test_annotationlib.MyClass") + self.assertEqual(value_to_source(len), "len") + self.assertEqual(value_to_source(value_to_source), "value_to_source") + self.assertEqual(value_to_source(times_three), "times_three") + self.assertEqual(value_to_source(...), "...") + self.assertEqual(value_to_source(None), "None") + self.assertEqual(value_to_source(1), "1") + self.assertEqual(value_to_source("1"), "'1'") + self.assertEqual(value_to_source(Format.VALUE), repr(Format.VALUE)) + self.assertEqual(value_to_source(MyClass()), "my repr") + + def test_annotations_to_source(self): + self.assertEqual(annotations_to_source({}), {}) + self.assertEqual(annotations_to_source({"x": int}), {"x": "int"}) + self.assertEqual(annotations_to_source({"x": "int"}), {"x": "int"}) + self.assertEqual( + annotations_to_source({"x": int, "y": str}), {"x": "int", "y": "str"} + ) + + class TestAnnotationLib(unittest.TestCase): def test__all__(self): support.check__all__(self, annotationlib) diff --git a/Lib/typing.py b/Lib/typing.py index 9377e77..252eef3 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -242,21 +242,10 @@ def _type_repr(obj): typically enough to uniquely identify a type. For everything else, we fall back on repr(obj). """ - # When changing this function, don't forget about - # `_collections_abc._type_repr`, which does the same thing - # and must be consistent with this one. - if isinstance(obj, type): - if obj.__module__ == 'builtins': - return obj.__qualname__ - return f'{obj.__module__}.{obj.__qualname__}' - if obj is ...: - return '...' - if isinstance(obj, types.FunctionType): - return obj.__name__ if isinstance(obj, tuple): # Special case for `repr` of types with `ParamSpec`: return '[' + ', '.join(_type_repr(t) for t in obj) + ']' - return repr(obj) + return annotationlib.value_to_source(obj) def _collect_type_parameters(args, *, enforce_default_ordering: bool = True): @@ -2948,14 +2937,10 @@ def _make_eager_annotate(types): if format in (annotationlib.Format.VALUE, annotationlib.Format.FORWARDREF): return checked_types else: - return _convert_to_source(types) + return annotationlib.annotations_to_source(types) return annotate -def _convert_to_source(types): - return {n: t if isinstance(t, str) else _type_repr(t) for n, t in types.items()} - - # attributes prohibited to set in NamedTuple class syntax _prohibited = frozenset({'__new__', '__init__', '__slots__', '__getnewargs__', '_fields', '_field_defaults', @@ -3241,7 +3226,7 @@ class _TypedDictMeta(type): for n, tp in own.items() } elif format == annotationlib.Format.SOURCE: - own = _convert_to_source(own_annotations) + own = annotationlib.annotations_to_source(own_annotations) else: own = own_checked_annotations annos.update(own) |