mirror of
https://github.com/python/cpython.git
synced 2025-12-31 04:23:37 +00:00
gh-124412: Add helpers for converting annotations to source format (#124551)
This commit is contained in:
parent
0268b072d8
commit
4e829c0e6f
5 changed files with 113 additions and 42 deletions
|
|
@ -197,6 +197,27 @@ Classes
|
|||
Functions
|
||||
---------
|
||||
|
||||
.. function:: annotations_to_source(annotations)
|
||||
|
||||
Convert an annotations dict containing runtime values to a
|
||||
dict containing only strings. If the values are not already strings,
|
||||
they are converted using :func:`value_to_source`.
|
||||
This is meant as a helper for user-provided
|
||||
annotate functions that support the :attr:`~Format.SOURCE` format but
|
||||
do not have access to the code creating the annotations.
|
||||
|
||||
For example, this is used to implement the :attr:`~Format.SOURCE` for
|
||||
:class:`typing.TypedDict` classes created through the functional syntax:
|
||||
|
||||
.. doctest::
|
||||
|
||||
>>> from typing import TypedDict
|
||||
>>> Movie = TypedDict("movie", {"name": str, "year": int})
|
||||
>>> get_annotations(Movie, format=Format.SOURCE)
|
||||
{'name': 'str', 'year': 'int'}
|
||||
|
||||
.. versionadded:: 3.14
|
||||
|
||||
.. function:: call_annotate_function(annotate, format, *, owner=None)
|
||||
|
||||
Call the :term:`annotate function` *annotate* with the given *format*,
|
||||
|
|
@ -347,3 +368,18 @@ Functions
|
|||
{'a': <class 'int'>, 'b': <class 'str'>, 'return': <class 'float'>}
|
||||
|
||||
.. versionadded:: 3.14
|
||||
|
||||
.. function:: value_to_source(value)
|
||||
|
||||
Convert an arbitrary Python value to a format suitable for use by the
|
||||
:attr:`~Format.SOURCE` format. This calls :func:`repr` for most
|
||||
objects, but has special handling for some objects, such as type objects.
|
||||
|
||||
This is meant as a helper for user-provided
|
||||
annotate functions that support the :attr:`~Format.SOURCE` format but
|
||||
do not have access to the code creating the annotations. It can also
|
||||
be used to provide a user-friendly string representation for other
|
||||
objects that contain values that are commonly encountered in annotations.
|
||||
|
||||
.. versionadded:: 3.14
|
||||
|
||||
|
|
|
|||
|
|
@ -485,9 +485,10 @@ def __new__(cls, origin, args):
|
|||
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):
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
"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:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,14 @@
|
|||
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 wrapper(a, b):
|
|||
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 @@ def test_name_lookup_without_eval(self):
|
|||
# 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 @@ def __annotations__(self):
|
|||
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 C:
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -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 annotate(format):
|
|||
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 @@ def __annotate__(format):
|
|||
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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue