Skip to content

Commit e239100

Browse files
Add TypeExpr (#430)
1 parent ece1201 commit e239100

File tree

4 files changed

+117
-0
lines changed

4 files changed

+117
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Unreleased
22

3+
- Add `typing_extensions.TypeExpr` from PEP 747. Patch by
4+
Jelle Zijlstra.
35
- Add `typing_extensions.get_annotations`, a backport of
46
`inspect.get_annotations` that adds features specified
57
by PEP 649. Patches by Jelle Zijlstra and Alex Waygood.

doc/index.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,12 @@ Special typing primitives
367367

368368
.. versionadded:: 4.6.0
369369

370+
.. data:: TypeExpr
371+
372+
See :pep:`747`. A type hint representing a type expression.
373+
374+
.. versionadded:: 4.13.0
375+
370376
.. data:: TypeGuard
371377

372378
See :py:data:`typing.TypeGuard` and :pep:`647`. In ``typing`` since 3.10.

src/test_typing_extensions.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
TypeAlias,
6969
TypeAliasType,
7070
TypedDict,
71+
TypeExpr,
7172
TypeGuard,
7273
TypeIs,
7374
TypeVar,
@@ -5468,6 +5469,64 @@ def test_no_isinstance(self):
54685469
issubclass(int, TypeIs)
54695470

54705471

5472+
class TypeExprTests(BaseTestCase):
5473+
def test_basics(self):
5474+
TypeExpr[int] # OK
5475+
self.assertEqual(TypeExpr[int], TypeExpr[int])
5476+
5477+
def foo(arg) -> TypeExpr[int]: ...
5478+
self.assertEqual(gth(foo), {'return': TypeExpr[int]})
5479+
5480+
def test_repr(self):
5481+
if hasattr(typing, 'TypeExpr'):
5482+
mod_name = 'typing'
5483+
else:
5484+
mod_name = 'typing_extensions'
5485+
self.assertEqual(repr(TypeExpr), f'{mod_name}.TypeExpr')
5486+
cv = TypeExpr[int]
5487+
self.assertEqual(repr(cv), f'{mod_name}.TypeExpr[int]')
5488+
cv = TypeExpr[Employee]
5489+
self.assertEqual(repr(cv), f'{mod_name}.TypeExpr[{__name__}.Employee]')
5490+
cv = TypeExpr[Tuple[int]]
5491+
self.assertEqual(repr(cv), f'{mod_name}.TypeExpr[typing.Tuple[int]]')
5492+
5493+
def test_cannot_subclass(self):
5494+
with self.assertRaises(TypeError):
5495+
class C(type(TypeExpr)):
5496+
pass
5497+
with self.assertRaises(TypeError):
5498+
class D(type(TypeExpr[int])):
5499+
pass
5500+
5501+
def test_call(self):
5502+
objs = [
5503+
1,
5504+
"int",
5505+
int,
5506+
Tuple[int, str],
5507+
]
5508+
for obj in objs:
5509+
with self.subTest(obj=obj):
5510+
self.assertIs(TypeExpr(obj), obj)
5511+
5512+
with self.assertRaises(TypeError):
5513+
TypeExpr()
5514+
with self.assertRaises(TypeError):
5515+
TypeExpr("too", "many")
5516+
5517+
def test_cannot_init_type(self):
5518+
with self.assertRaises(TypeError):
5519+
type(TypeExpr)()
5520+
with self.assertRaises(TypeError):
5521+
type(TypeExpr[Optional[int]])()
5522+
5523+
def test_no_isinstance(self):
5524+
with self.assertRaises(TypeError):
5525+
isinstance(1, TypeExpr[int])
5526+
with self.assertRaises(TypeError):
5527+
issubclass(int, TypeExpr)
5528+
5529+
54715530
class LiteralStringTests(BaseTestCase):
54725531
def test_basics(self):
54735532
class Foo:

src/typing_extensions.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
'Text',
8787
'TypeAlias',
8888
'TypeAliasType',
89+
'TypeExpr',
8990
'TypeGuard',
9091
'TypeIs',
9192
'TYPE_CHECKING',
@@ -2045,6 +2046,55 @@ def f(val: Union[int, Awaitable[int]]) -> int:
20452046
PEP 742 (Narrowing types with TypeIs).
20462047
""")
20472048

2049+
# 3.14+?
2050+
if hasattr(typing, 'TypeExpr'):
2051+
TypeExpr = typing.TypeExpr
2052+
# 3.9
2053+
elif sys.version_info[:2] >= (3, 9):
2054+
class _TypeExprForm(_ExtensionsSpecialForm, _root=True):
2055+
# TypeExpr(X) is equivalent to X but indicates to the type checker
2056+
# that the object is a TypeExpr.
2057+
def __call__(self, obj, /):
2058+
return obj
2059+
2060+
@_TypeExprForm
2061+
def TypeExpr(self, parameters):
2062+
"""Special typing form used to represent a type expression.
2063+
2064+
Usage:
2065+
2066+
def cast[T](typ: TypeExpr[T], value: Any) -> T: ...
2067+
2068+
reveal_type(cast(int, "x")) # int
2069+
2070+
See PEP 747 for more information.
2071+
"""
2072+
item = typing._type_check(parameters, f'{self} accepts only a single type.')
2073+
return typing._GenericAlias(self, (item,))
2074+
# 3.8
2075+
else:
2076+
class _TypeExprForm(_ExtensionsSpecialForm, _root=True):
2077+
def __getitem__(self, parameters):
2078+
item = typing._type_check(parameters,
2079+
f'{self._name} accepts only a single type')
2080+
return typing._GenericAlias(self, (item,))
2081+
2082+
def __call__(self, obj, /):
2083+
return obj
2084+
2085+
TypeExpr = _TypeExprForm(
2086+
'TypeExpr',
2087+
doc="""Special typing form used to represent a type expression.
2088+
2089+
Usage:
2090+
2091+
def cast[T](typ: TypeExpr[T], value: Any) -> T: ...
2092+
2093+
reveal_type(cast(int, "x")) # int
2094+
2095+
See PEP 747 for more information.
2096+
""")
2097+
20482098

20492099
# Vendored from cpython typing._SpecialFrom
20502100
class _SpecialForm(typing._Final, _root=True):

0 commit comments

Comments
 (0)