diff --git a/CHANGELOG.md b/CHANGELOG.md index 8855595e..cb143299 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# Unreleased + +- Add `typing_extensions.type_repr`, a backport of + [`annotationlib.type_repr`](https://docs.python.org/3.14/library/annotationlib.html#annotationlib.type_repr), + introduced in Python 3.14 (CPython PR [#124551](https://github.com/python/cpython/pull/124551), + originally by Jelle Zijlstra). Patch by Semyon Moroz. + + # Release 4.14.1 (July 4, 2025) - Fix usage of `typing_extensions.TypedDict` nested inside other types diff --git a/doc/index.rst b/doc/index.rst index 21d6fa60..c22336fb 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -933,6 +933,15 @@ Functions .. versionadded:: 4.1.0 +.. function:: type_repr(value) + + See :py:func:`annotationlib.type_repr`. In ``annotationlib`` since 3.14. + + Convert an arbitrary Python value to a format suitable for use by + the :attr:`Format.STRING`. + + .. versionadded:: 4.15.0 + Enums ~~~~~ diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index cb3b462b..7a6380a3 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -101,6 +101,7 @@ reveal_type, runtime, runtime_checkable, + type_repr, ) NoneType = type(None) @@ -8368,6 +8369,44 @@ def test_capsule_type(self): self.assertIsInstance(_datetime.datetime_CAPI, typing_extensions.CapsuleType) +class MyClass: + def __repr__(self): + return "my repr" + + +class TestTypeRepr(BaseTestCase): + def test_custom_types(self): + + class Nested: + pass + + def nested(): + pass + + self.assertEqual(type_repr(MyClass), f"{__name__}.MyClass") + self.assertEqual( + type_repr(Nested), + f"{__name__}.TestTypeRepr.test_custom_types..Nested", + ) + self.assertEqual( + type_repr(nested), + f"{__name__}.TestTypeRepr.test_custom_types..nested", + ) + self.assertEqual(type_repr(times_three), f"{__name__}.times_three") + self.assertEqual(type_repr(Format.VALUE), repr(Format.VALUE)) + self.assertEqual(type_repr(MyClass()), "my repr") + + def test_builtin_types(self): + self.assertEqual(type_repr(int), "int") + self.assertEqual(type_repr(object), "object") + self.assertEqual(type_repr(None), "None") + self.assertEqual(type_repr(len), "len") + self.assertEqual(type_repr(1), "1") + self.assertEqual(type_repr("1"), "'1'") + self.assertEqual(type_repr(''), "''") + self.assertEqual(type_repr(...), "...") + + def times_three(fn): @functools.wraps(fn) def wrapper(a, b): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index efa09d55..7f838700 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -100,6 +100,7 @@ 'TypeGuard', 'TypeIs', 'TYPE_CHECKING', + 'type_repr', 'Never', 'NoReturn', 'ReadOnly', @@ -4192,6 +4193,26 @@ def __getstate__(self): raise TypeError(f"Cannot pickle {type(self).__name__!r} object") +if sys.version_info >= (3, 14, 0, "beta"): + type_repr = annotationlib.type_repr +else: + def type_repr(value): + """Convert a Python value to a format suitable for use with the STRING format. + + This is intended as a helper for tools that support the STRING format but do + not have access to the code that originally produced the annotations. It uses + repr() for most objects. + + """ + if isinstance(value, (type, _types.FunctionType, _types.BuiltinFunctionType)): + if value.__module__ == "builtins": + return value.__qualname__ + return f"{value.__module__}.{value.__qualname__}" + if value is ...: + return "..." + return repr(value) + + # Aliases for items that are in typing in all supported versions. # We use hasattr() checks so this library will continue to import on # future versions of Python that may remove these names.