summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorJelle Zijlstra <jelle.zijlstra@gmail.com>2024-09-26 00:01:09 (GMT)
committerGitHub <noreply@github.com>2024-09-26 00:01:09 (GMT)
commit4e829c0e6fbd47818451660c234eacc42a0b4a08 (patch)
tree20f181a657ba4a3e65fe90c5971d04105d4bf9b5 /Lib
parent0268b072d84bc4be890d1b7459815ba1cb9f9945 (diff)
downloadcpython-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.py22
-rw-r--r--Lib/annotationlib.py31
-rw-r--r--Lib/test/test_annotationlib.py45
-rw-r--r--Lib/typing.py21
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)