Skip to content

Commit 566e01e

Browse files
Add support for TypeIs (PEP 742) (#330)
* Add support for TypeNarrower (PEP 742) * Use TypeIs
1 parent d6c50f5 commit 566e01e

File tree

4 files changed

+146
-1
lines changed

4 files changed

+146
-1
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 support for PEP 742, adding `typing_extensions.TypeIs`. Patch
4+
by Jelle Zijlstra.
35
- Drop runtime error when a mutable `TypedDict` key overrides a read-only
46
one. Type checkers should still flag this as an error. Patch by Jelle
57
Zijlstra.

doc/index.rst

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

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

353+
.. data:: TypeIs
354+
355+
See :pep:`742`. Similar to :data:`TypeGuard`, but allows more type narrowing.
356+
357+
.. versionadded:: 4.10.0
358+
353359
.. class:: TypedDict(dict, total=True)
354360

355361
See :py:class:`typing.TypedDict` and :pep:`589`. In ``typing`` since 3.8.

src/test_typing_extensions.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from typing_extensions import TypeVarTuple, Unpack, dataclass_transform, reveal_type, Never, assert_never, LiteralString
3737
from typing_extensions import assert_type, get_type_hints, get_origin, get_args, get_original_bases
3838
from typing_extensions import clear_overloads, get_overloads, overload
39-
from typing_extensions import NamedTuple
39+
from typing_extensions import NamedTuple, TypeIs
4040
from typing_extensions import override, deprecated, Buffer, TypeAliasType, TypeVar, get_protocol_members, is_protocol
4141
from typing_extensions import Doc
4242
from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated
@@ -4774,6 +4774,50 @@ def test_no_isinstance(self):
47744774
issubclass(int, TypeGuard)
47754775

47764776

4777+
class TypeIsTests(BaseTestCase):
4778+
def test_basics(self):
4779+
TypeIs[int] # OK
4780+
self.assertEqual(TypeIs[int], TypeIs[int])
4781+
4782+
def foo(arg) -> TypeIs[int]: ...
4783+
self.assertEqual(gth(foo), {'return': TypeIs[int]})
4784+
4785+
def test_repr(self):
4786+
if hasattr(typing, 'TypeIs'):
4787+
mod_name = 'typing'
4788+
else:
4789+
mod_name = 'typing_extensions'
4790+
self.assertEqual(repr(TypeIs), f'{mod_name}.TypeIs')
4791+
cv = TypeIs[int]
4792+
self.assertEqual(repr(cv), f'{mod_name}.TypeIs[int]')
4793+
cv = TypeIs[Employee]
4794+
self.assertEqual(repr(cv), f'{mod_name}.TypeIs[{__name__}.Employee]')
4795+
cv = TypeIs[Tuple[int]]
4796+
self.assertEqual(repr(cv), f'{mod_name}.TypeIs[typing.Tuple[int]]')
4797+
4798+
def test_cannot_subclass(self):
4799+
with self.assertRaises(TypeError):
4800+
class C(type(TypeIs)):
4801+
pass
4802+
with self.assertRaises(TypeError):
4803+
class C(type(TypeIs[int])):
4804+
pass
4805+
4806+
def test_cannot_init(self):
4807+
with self.assertRaises(TypeError):
4808+
TypeIs()
4809+
with self.assertRaises(TypeError):
4810+
type(TypeIs)()
4811+
with self.assertRaises(TypeError):
4812+
type(TypeIs[Optional[int]])()
4813+
4814+
def test_no_isinstance(self):
4815+
with self.assertRaises(TypeError):
4816+
isinstance(1, TypeIs[int])
4817+
with self.assertRaises(TypeError):
4818+
issubclass(int, TypeIs)
4819+
4820+
47774821
class LiteralStringTests(BaseTestCase):
47784822
def test_basics(self):
47794823
class Foo:

src/typing_extensions.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
'TypeAlias',
8484
'TypeAliasType',
8585
'TypeGuard',
86+
'TypeIs',
8687
'TYPE_CHECKING',
8788
'Never',
8889
'NoReturn',
@@ -1822,6 +1823,98 @@ def is_str(val: Union[str, float]):
18221823
PEP 647 (User-Defined Type Guards).
18231824
""")
18241825

1826+
# 3.13+
1827+
if hasattr(typing, 'TypeIs'):
1828+
TypeIs = typing.TypeIs
1829+
# 3.9
1830+
elif sys.version_info[:2] >= (3, 9):
1831+
@_ExtensionsSpecialForm
1832+
def TypeIs(self, parameters):
1833+
"""Special typing form used to annotate the return type of a user-defined
1834+
type narrower function. ``TypeIs`` only accepts a single type argument.
1835+
At runtime, functions marked this way should return a boolean.
1836+
1837+
``TypeIs`` aims to benefit *type narrowing* -- a technique used by static
1838+
type checkers to determine a more precise type of an expression within a
1839+
program's code flow. Usually type narrowing is done by analyzing
1840+
conditional code flow and applying the narrowing to a block of code. The
1841+
conditional expression here is sometimes referred to as a "type guard".
1842+
1843+
Sometimes it would be convenient to use a user-defined boolean function
1844+
as a type guard. Such a function should use ``TypeIs[...]`` as its
1845+
return type to alert static type checkers to this intention.
1846+
1847+
Using ``-> TypeIs`` tells the static type checker that for a given
1848+
function:
1849+
1850+
1. The return value is a boolean.
1851+
2. If the return value is ``True``, the type of its argument
1852+
is the intersection of the type inside ``TypeGuard`` and the argument's
1853+
previously known type.
1854+
1855+
For example::
1856+
1857+
def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]:
1858+
return hasattr(val, '__await__')
1859+
1860+
def f(val: Union[int, Awaitable[int]]) -> int:
1861+
if is_awaitable(val):
1862+
assert_type(val, Awaitable[int])
1863+
else:
1864+
assert_type(val, int)
1865+
1866+
``TypeIs`` also works with type variables. For more information, see
1867+
PEP 742 (Narrowing types with TypeIs).
1868+
"""
1869+
item = typing._type_check(parameters, f'{self} accepts only a single type.')
1870+
return typing._GenericAlias(self, (item,))
1871+
# 3.8
1872+
else:
1873+
class _TypeIsForm(_ExtensionsSpecialForm, _root=True):
1874+
def __getitem__(self, parameters):
1875+
item = typing._type_check(parameters,
1876+
f'{self._name} accepts only a single type')
1877+
return typing._GenericAlias(self, (item,))
1878+
1879+
TypeIs = _TypeIsForm(
1880+
'TypeIs',
1881+
doc="""Special typing form used to annotate the return type of a user-defined
1882+
type narrower function. ``TypeIs`` only accepts a single type argument.
1883+
At runtime, functions marked this way should return a boolean.
1884+
1885+
``TypeIs`` aims to benefit *type narrowing* -- a technique used by static
1886+
type checkers to determine a more precise type of an expression within a
1887+
program's code flow. Usually type narrowing is done by analyzing
1888+
conditional code flow and applying the narrowing to a block of code. The
1889+
conditional expression here is sometimes referred to as a "type guard".
1890+
1891+
Sometimes it would be convenient to use a user-defined boolean function
1892+
as a type guard. Such a function should use ``TypeIs[...]`` as its
1893+
return type to alert static type checkers to this intention.
1894+
1895+
Using ``-> TypeIs`` tells the static type checker that for a given
1896+
function:
1897+
1898+
1. The return value is a boolean.
1899+
2. If the return value is ``True``, the type of its argument
1900+
is the intersection of the type inside ``TypeGuard`` and the argument's
1901+
previously known type.
1902+
1903+
For example::
1904+
1905+
def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]:
1906+
return hasattr(val, '__await__')
1907+
1908+
def f(val: Union[int, Awaitable[int]]) -> int:
1909+
if is_awaitable(val):
1910+
assert_type(val, Awaitable[int])
1911+
else:
1912+
assert_type(val, int)
1913+
1914+
``TypeIs`` also works with type variables. For more information, see
1915+
PEP 742 (Narrowing types with TypeIs).
1916+
""")
1917+
18251918

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

0 commit comments

Comments
 (0)