Skip to content

Commit

Permalink
Add support for TypeIs (PEP 742) (#330)
Browse files Browse the repository at this point in the history
* Add support for TypeNarrower (PEP 742)

* Use TypeIs
  • Loading branch information
JelleZijlstra authored Feb 16, 2024
1 parent d6c50f5 commit 566e01e
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Unreleased

- Add support for PEP 742, adding `typing_extensions.TypeIs`. Patch
by Jelle Zijlstra.
- Drop runtime error when a mutable `TypedDict` key overrides a read-only
one. Type checkers should still flag this as an error. Patch by Jelle
Zijlstra.
Expand Down
6 changes: 6 additions & 0 deletions doc/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,12 @@ Special typing primitives

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

.. data:: TypeIs

See :pep:`742`. Similar to :data:`TypeGuard`, but allows more type narrowing.

.. versionadded:: 4.10.0

.. class:: TypedDict(dict, total=True)

See :py:class:`typing.TypedDict` and :pep:`589`. In ``typing`` since 3.8.
Expand Down
46 changes: 45 additions & 1 deletion src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
from typing_extensions import TypeVarTuple, Unpack, dataclass_transform, reveal_type, Never, assert_never, LiteralString
from typing_extensions import assert_type, get_type_hints, get_origin, get_args, get_original_bases
from typing_extensions import clear_overloads, get_overloads, overload
from typing_extensions import NamedTuple
from typing_extensions import NamedTuple, TypeIs
from typing_extensions import override, deprecated, Buffer, TypeAliasType, TypeVar, get_protocol_members, is_protocol
from typing_extensions import Doc
from _typed_dict_test_helper import Foo, FooGeneric, VeryAnnotated
Expand Down Expand Up @@ -4774,6 +4774,50 @@ def test_no_isinstance(self):
issubclass(int, TypeGuard)


class TypeIsTests(BaseTestCase):
def test_basics(self):
TypeIs[int] # OK
self.assertEqual(TypeIs[int], TypeIs[int])

def foo(arg) -> TypeIs[int]: ...
self.assertEqual(gth(foo), {'return': TypeIs[int]})

def test_repr(self):
if hasattr(typing, 'TypeIs'):
mod_name = 'typing'
else:
mod_name = 'typing_extensions'
self.assertEqual(repr(TypeIs), f'{mod_name}.TypeIs')
cv = TypeIs[int]
self.assertEqual(repr(cv), f'{mod_name}.TypeIs[int]')
cv = TypeIs[Employee]
self.assertEqual(repr(cv), f'{mod_name}.TypeIs[{__name__}.Employee]')
cv = TypeIs[Tuple[int]]
self.assertEqual(repr(cv), f'{mod_name}.TypeIs[typing.Tuple[int]]')

def test_cannot_subclass(self):
with self.assertRaises(TypeError):
class C(type(TypeIs)):
pass
with self.assertRaises(TypeError):
class C(type(TypeIs[int])):
pass

def test_cannot_init(self):
with self.assertRaises(TypeError):
TypeIs()
with self.assertRaises(TypeError):
type(TypeIs)()
with self.assertRaises(TypeError):
type(TypeIs[Optional[int]])()

def test_no_isinstance(self):
with self.assertRaises(TypeError):
isinstance(1, TypeIs[int])
with self.assertRaises(TypeError):
issubclass(int, TypeIs)


class LiteralStringTests(BaseTestCase):
def test_basics(self):
class Foo:
Expand Down
93 changes: 93 additions & 0 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
'TypeAlias',
'TypeAliasType',
'TypeGuard',
'TypeIs',
'TYPE_CHECKING',
'Never',
'NoReturn',
Expand Down Expand Up @@ -1822,6 +1823,98 @@ def is_str(val: Union[str, float]):
PEP 647 (User-Defined Type Guards).
""")

# 3.13+
if hasattr(typing, 'TypeIs'):
TypeIs = typing.TypeIs
# 3.9
elif sys.version_info[:2] >= (3, 9):
@_ExtensionsSpecialForm
def TypeIs(self, parameters):
"""Special typing form used to annotate the return type of a user-defined
type narrower function. ``TypeIs`` only accepts a single type argument.
At runtime, functions marked this way should return a boolean.
``TypeIs`` aims to benefit *type narrowing* -- a technique used by static
type checkers to determine a more precise type of an expression within a
program's code flow. Usually type narrowing is done by analyzing
conditional code flow and applying the narrowing to a block of code. The
conditional expression here is sometimes referred to as a "type guard".
Sometimes it would be convenient to use a user-defined boolean function
as a type guard. Such a function should use ``TypeIs[...]`` as its
return type to alert static type checkers to this intention.
Using ``-> TypeIs`` tells the static type checker that for a given
function:
1. The return value is a boolean.
2. If the return value is ``True``, the type of its argument
is the intersection of the type inside ``TypeGuard`` and the argument's
previously known type.
For example::
def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]:
return hasattr(val, '__await__')
def f(val: Union[int, Awaitable[int]]) -> int:
if is_awaitable(val):
assert_type(val, Awaitable[int])
else:
assert_type(val, int)
``TypeIs`` also works with type variables. For more information, see
PEP 742 (Narrowing types with TypeIs).
"""
item = typing._type_check(parameters, f'{self} accepts only a single type.')
return typing._GenericAlias(self, (item,))
# 3.8
else:
class _TypeIsForm(_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,))

TypeIs = _TypeIsForm(
'TypeIs',
doc="""Special typing form used to annotate the return type of a user-defined
type narrower function. ``TypeIs`` only accepts a single type argument.
At runtime, functions marked this way should return a boolean.
``TypeIs`` aims to benefit *type narrowing* -- a technique used by static
type checkers to determine a more precise type of an expression within a
program's code flow. Usually type narrowing is done by analyzing
conditional code flow and applying the narrowing to a block of code. The
conditional expression here is sometimes referred to as a "type guard".
Sometimes it would be convenient to use a user-defined boolean function
as a type guard. Such a function should use ``TypeIs[...]`` as its
return type to alert static type checkers to this intention.
Using ``-> TypeIs`` tells the static type checker that for a given
function:
1. The return value is a boolean.
2. If the return value is ``True``, the type of its argument
is the intersection of the type inside ``TypeGuard`` and the argument's
previously known type.
For example::
def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]:
return hasattr(val, '__await__')
def f(val: Union[int, Awaitable[int]]) -> int:
if is_awaitable(val):
assert_type(val, Awaitable[int])
else:
assert_type(val, int)
``TypeIs`` also works with type variables. For more information, see
PEP 742 (Narrowing types with TypeIs).
""")


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

0 comments on commit 566e01e

Please sign in to comment.