From 566e01e7a798abfcf88849814918fd8413b8d18b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 15 Feb 2024 20:21:55 -0800 Subject: [PATCH] Add support for TypeIs (PEP 742) (#330) * Add support for TypeNarrower (PEP 742) * Use TypeIs --- CHANGELOG.md | 2 + doc/index.rst | 6 +++ src/test_typing_extensions.py | 46 ++++++++++++++++- src/typing_extensions.py | 93 +++++++++++++++++++++++++++++++++++ 4 files changed, 146 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e052710c..8be5ebd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/doc/index.rst b/doc/index.rst index 4985762e..b1e2477b 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -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. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index 2e8fe7b3..12677fd0 100644 --- a/src/test_typing_extensions.py +++ b/src/test_typing_extensions.py @@ -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 @@ -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: diff --git a/src/typing_extensions.py b/src/typing_extensions.py index a016e07b..3ef09259 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -83,6 +83,7 @@ 'TypeAlias', 'TypeAliasType', 'TypeGuard', + 'TypeIs', 'TYPE_CHECKING', 'Never', 'NoReturn', @@ -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):