Skip to content

Commit

Permalink
Add TypeExpr (#430)
Browse files Browse the repository at this point in the history
  • Loading branch information
JelleZijlstra authored Jun 22, 2024
1 parent ece1201 commit e239100
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
6 changes: 6 additions & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
59 changes: 59 additions & 0 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
TypeAlias,
TypeAliasType,
TypedDict,
TypeExpr,
TypeGuard,
TypeIs,
TypeVar,
Expand Down Expand Up @@ -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:
Expand Down
50 changes: 50 additions & 0 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
'Text',
'TypeAlias',
'TypeAliasType',
'TypeExpr',
'TypeGuard',
'TypeIs',
'TYPE_CHECKING',
Expand Down Expand Up @@ -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):
Expand Down

0 comments on commit e239100

Please sign in to comment.