From e239100563edd4699d6cc74bdfe27b50aad88d61 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 22 Jun 2024 00:03:32 -0700 Subject: [PATCH] Add TypeExpr (#430) --- CHANGELOG.md | 2 ++ doc/index.rst | 6 ++++ src/test_typing_extensions.py | 59 +++++++++++++++++++++++++++++++++++ src/typing_extensions.py | 50 +++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0226826..68c4cf34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Add `typing_extensions.TypeExpr` from PEP 747. Patch by + Jelle Zijlstra. - Add `typing_extensions.get_annotations`, a backport of `inspect.get_annotations` that adds features specified by PEP 649. Patches by Jelle Zijlstra and Alex Waygood. diff --git a/doc/index.rst b/doc/index.rst index 15c9c8d5..23a531c4 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -367,6 +367,12 @@ Special typing primitives .. versionadded:: 4.6.0 +.. data:: TypeExpr + + See :pep:`747`. A type hint representing a type expression. + + .. versionadded:: 4.13.0 + .. data:: TypeGuard See :py:data:`typing.TypeGuard` and :pep:`647`. In ``typing`` since 3.10. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 362845fe..868e7938 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -68,6 +68,7 @@ TypeAlias, TypeAliasType, TypedDict, + TypeExpr, TypeGuard, TypeIs, TypeVar, @@ -5468,6 +5469,64 @@ def test_no_isinstance(self): issubclass(int, TypeIs) +class TypeExprTests(BaseTestCase): + def test_basics(self): + TypeExpr[int] # OK + self.assertEqual(TypeExpr[int], TypeExpr[int]) + + def foo(arg) -> TypeExpr[int]: ... + self.assertEqual(gth(foo), {'return': TypeExpr[int]}) + + def test_repr(self): + if hasattr(typing, 'TypeExpr'): + mod_name = 'typing' + else: + mod_name = 'typing_extensions' + self.assertEqual(repr(TypeExpr), f'{mod_name}.TypeExpr') + cv = TypeExpr[int] + self.assertEqual(repr(cv), f'{mod_name}.TypeExpr[int]') + cv = TypeExpr[Employee] + self.assertEqual(repr(cv), f'{mod_name}.TypeExpr[{__name__}.Employee]') + cv = TypeExpr[Tuple[int]] + self.assertEqual(repr(cv), f'{mod_name}.TypeExpr[typing.Tuple[int]]') + + def test_cannot_subclass(self): + with self.assertRaises(TypeError): + class C(type(TypeExpr)): + pass + with self.assertRaises(TypeError): + class D(type(TypeExpr[int])): + pass + + def test_call(self): + objs = [ + 1, + "int", + int, + Tuple[int, str], + ] + for obj in objs: + with self.subTest(obj=obj): + self.assertIs(TypeExpr(obj), obj) + + with self.assertRaises(TypeError): + TypeExpr() + with self.assertRaises(TypeError): + TypeExpr("too", "many") + + def test_cannot_init_type(self): + with self.assertRaises(TypeError): + type(TypeExpr)() + with self.assertRaises(TypeError): + type(TypeExpr[Optional[int]])() + + def test_no_isinstance(self): + with self.assertRaises(TypeError): + isinstance(1, TypeExpr[int]) + with self.assertRaises(TypeError): + issubclass(int, TypeExpr) + + class LiteralStringTests(BaseTestCase): def test_basics(self): class Foo: diff --git a/src/typing_extensions.py b/src/typing_extensions.py index d5d0a115..8046dae1 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -86,6 +86,7 @@ 'Text', 'TypeAlias', 'TypeAliasType', + 'TypeExpr', 'TypeGuard', 'TypeIs', 'TYPE_CHECKING', @@ -2045,6 +2046,55 @@ def f(val: Union[int, Awaitable[int]]) -> int: PEP 742 (Narrowing types with TypeIs). """) +# 3.14+? +if hasattr(typing, 'TypeExpr'): + TypeExpr = typing.TypeExpr +# 3.9 +elif sys.version_info[:2] >= (3, 9): + class _TypeExprForm(_ExtensionsSpecialForm, _root=True): + # TypeExpr(X) is equivalent to X but indicates to the type checker + # that the object is a TypeExpr. + def __call__(self, obj, /): + return obj + + @_TypeExprForm + def TypeExpr(self, parameters): + """Special typing form used to represent a type expression. + + Usage: + + def cast[T](typ: TypeExpr[T], value: Any) -> T: ... + + reveal_type(cast(int, "x")) # int + + See PEP 747 for more information. + """ + item = typing._type_check(parameters, f'{self} accepts only a single type.') + return typing._GenericAlias(self, (item,)) +# 3.8 +else: + class _TypeExprForm(_ExtensionsSpecialForm, _root=True): + def __getitem__(self, parameters): + item = typing._type_check(parameters, + f'{self._name} accepts only a single type') + return typing._GenericAlias(self, (item,)) + + def __call__(self, obj, /): + return obj + + TypeExpr = _TypeExprForm( + 'TypeExpr', + doc="""Special typing form used to represent a type expression. + + Usage: + + def cast[T](typ: TypeExpr[T], value: Any) -> T: ... + + reveal_type(cast(int, "x")) # int + + See PEP 747 for more information. + """) + # Vendored from cpython typing._SpecialFrom class _SpecialForm(typing._Final, _root=True):