From a1cebaf90aa65e706831aed283b56a6bf795e607 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 8 Feb 2024 19:10:13 -0800 Subject: [PATCH 1/2] Add support for TypeNarrower (PEP 742) --- 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 be1c16d6..08a428a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +- Add support for PEP 742, adding `typing_extensions.TypeNarrower`. Patch + by Jelle Zijlstra. - Speedup `issubclass()` checks against simple runtime-checkable protocols by around 6% (backporting https://github.com/python/cpython/pull/112717, by Alex Waygood). diff --git a/doc/index.rst b/doc/index.rst index 4985762e..98606600 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:: TypeNarrower + + 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 58dc1851..f46e49f3 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, TypeNarrower 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 TypeNarrowerTests(BaseTestCase): + def test_basics(self): + TypeNarrower[int] # OK + self.assertEqual(TypeNarrower[int], TypeNarrower[int]) + + def foo(arg) -> TypeNarrower[int]: ... + self.assertEqual(gth(foo), {'return': TypeNarrower[int]}) + + def test_repr(self): + if hasattr(typing, 'TypeNarrower'): + mod_name = 'typing' + else: + mod_name = 'typing_extensions' + self.assertEqual(repr(TypeNarrower), f'{mod_name}.TypeNarrower') + cv = TypeNarrower[int] + self.assertEqual(repr(cv), f'{mod_name}.TypeNarrower[int]') + cv = TypeNarrower[Employee] + self.assertEqual(repr(cv), f'{mod_name}.TypeNarrower[{__name__}.Employee]') + cv = TypeNarrower[Tuple[int]] + self.assertEqual(repr(cv), f'{mod_name}.TypeNarrower[typing.Tuple[int]]') + + def test_cannot_subclass(self): + with self.assertRaises(TypeError): + class C(type(TypeNarrower)): + pass + with self.assertRaises(TypeError): + class C(type(TypeNarrower[int])): + pass + + def test_cannot_init(self): + with self.assertRaises(TypeError): + TypeNarrower() + with self.assertRaises(TypeError): + type(TypeNarrower)() + with self.assertRaises(TypeError): + type(TypeNarrower[Optional[int]])() + + def test_no_isinstance(self): + with self.assertRaises(TypeError): + isinstance(1, TypeNarrower[int]) + with self.assertRaises(TypeError): + issubclass(int, TypeNarrower) + + class LiteralStringTests(BaseTestCase): def test_basics(self): class Foo: diff --git a/src/typing_extensions.py b/src/typing_extensions.py index 4007594c..f3b51d03 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -83,6 +83,7 @@ 'TypeAlias', 'TypeAliasType', 'TypeGuard', + 'TypeNarrower', 'TYPE_CHECKING', 'Never', 'NoReturn', @@ -1827,6 +1828,98 @@ def is_str(val: Union[str, float]): PEP 647 (User-Defined Type Guards). """) +# 3.13+ +if hasattr(typing, 'TypeNarrower'): + TypeNarrower = typing.TypeNarrower +# 3.9 +elif sys.version_info[:2] >= (3, 9): + @_ExtensionsSpecialForm + def TypeNarrower(self, parameters): + """Special typing form used to annotate the return type of a user-defined + type narrower function. ``TypeNarrower`` only accepts a single type argument. + At runtime, functions marked this way should return a boolean. + + ``TypeNarrower`` 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 ``TypeNarrower[...]`` as its + return type to alert static type checkers to this intention. + + Using ``-> TypeNarrower`` 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) -> TypeNarrower[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) + + ``TypeNarrower`` also works with type variables. For more information, see + PEP 742 (Narrowing types with TypeNarrower). + """ + item = typing._type_check(parameters, f'{self} accepts only a single type.') + return typing._GenericAlias(self, (item,)) +# 3.8 +else: + class _TypeNarrowerForm(_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,)) + + TypeNarrower = _TypeNarrowerForm( + 'TypeNarrower', + doc="""Special typing form used to annotate the return type of a user-defined + type narrower function. ``TypeNarrower`` only accepts a single type argument. + At runtime, functions marked this way should return a boolean. + + ``TypeNarrower`` 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 ``TypeNarrower[...]`` as its + return type to alert static type checkers to this intention. + + Using ``-> TypeNarrower`` 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) -> TypeNarrower[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) + + ``TypeNarrower`` also works with type variables. For more information, see + PEP 742 (Narrowing types with TypeNarrower). + """) + # Vendored from cpython typing._SpecialFrom class _SpecialForm(typing._Final, _root=True): From 1e12fa85274aa187f06f756634f73c755d7ee52a Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Wed, 14 Feb 2024 08:19:33 -0800 Subject: [PATCH 2/2] Use TypeIs --- CHANGELOG.md | 2 +- doc/index.rst | 2 +- src/test_typing_extensions.py | 42 +++++++++++++++++------------------ src/typing_extensions.py | 42 +++++++++++++++++------------------ 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08a428a5..67662f19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Unreleased -- Add support for PEP 742, adding `typing_extensions.TypeNarrower`. Patch +- Add support for PEP 742, adding `typing_extensions.TypeIs`. Patch by Jelle Zijlstra. - Speedup `issubclass()` checks against simple runtime-checkable protocols by around 6% (backporting https://github.com/python/cpython/pull/112717, by Alex diff --git a/doc/index.rst b/doc/index.rst index 98606600..b1e2477b 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -350,7 +350,7 @@ Special typing primitives See :py:data:`typing.TypeGuard` and :pep:`647`. In ``typing`` since 3.10. -.. data:: TypeNarrower +.. data:: TypeIs See :pep:`742`. Similar to :data:`TypeGuard`, but allows more type narrowing. diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py index f46e49f3..ea476cab 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, TypeNarrower +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,48 +4774,48 @@ def test_no_isinstance(self): issubclass(int, TypeGuard) -class TypeNarrowerTests(BaseTestCase): +class TypeIsTests(BaseTestCase): def test_basics(self): - TypeNarrower[int] # OK - self.assertEqual(TypeNarrower[int], TypeNarrower[int]) + TypeIs[int] # OK + self.assertEqual(TypeIs[int], TypeIs[int]) - def foo(arg) -> TypeNarrower[int]: ... - self.assertEqual(gth(foo), {'return': TypeNarrower[int]}) + def foo(arg) -> TypeIs[int]: ... + self.assertEqual(gth(foo), {'return': TypeIs[int]}) def test_repr(self): - if hasattr(typing, 'TypeNarrower'): + if hasattr(typing, 'TypeIs'): mod_name = 'typing' else: mod_name = 'typing_extensions' - self.assertEqual(repr(TypeNarrower), f'{mod_name}.TypeNarrower') - cv = TypeNarrower[int] - self.assertEqual(repr(cv), f'{mod_name}.TypeNarrower[int]') - cv = TypeNarrower[Employee] - self.assertEqual(repr(cv), f'{mod_name}.TypeNarrower[{__name__}.Employee]') - cv = TypeNarrower[Tuple[int]] - self.assertEqual(repr(cv), f'{mod_name}.TypeNarrower[typing.Tuple[int]]') + 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(TypeNarrower)): + class C(type(TypeIs)): pass with self.assertRaises(TypeError): - class C(type(TypeNarrower[int])): + class C(type(TypeIs[int])): pass def test_cannot_init(self): with self.assertRaises(TypeError): - TypeNarrower() + TypeIs() with self.assertRaises(TypeError): - type(TypeNarrower)() + type(TypeIs)() with self.assertRaises(TypeError): - type(TypeNarrower[Optional[int]])() + type(TypeIs[Optional[int]])() def test_no_isinstance(self): with self.assertRaises(TypeError): - isinstance(1, TypeNarrower[int]) + isinstance(1, TypeIs[int]) with self.assertRaises(TypeError): - issubclass(int, TypeNarrower) + issubclass(int, TypeIs) class LiteralStringTests(BaseTestCase): diff --git a/src/typing_extensions.py b/src/typing_extensions.py index f3b51d03..35a9ca4b 100644 --- a/src/typing_extensions.py +++ b/src/typing_extensions.py @@ -83,7 +83,7 @@ 'TypeAlias', 'TypeAliasType', 'TypeGuard', - 'TypeNarrower', + 'TypeIs', 'TYPE_CHECKING', 'Never', 'NoReturn', @@ -1829,27 +1829,27 @@ def is_str(val: Union[str, float]): """) # 3.13+ -if hasattr(typing, 'TypeNarrower'): - TypeNarrower = typing.TypeNarrower +if hasattr(typing, 'TypeIs'): + TypeIs = typing.TypeIs # 3.9 elif sys.version_info[:2] >= (3, 9): @_ExtensionsSpecialForm - def TypeNarrower(self, parameters): + def TypeIs(self, parameters): """Special typing form used to annotate the return type of a user-defined - type narrower function. ``TypeNarrower`` only accepts a single type argument. + type narrower function. ``TypeIs`` only accepts a single type argument. At runtime, functions marked this way should return a boolean. - ``TypeNarrower`` aims to benefit *type narrowing* -- a technique used by static + ``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 ``TypeNarrower[...]`` as its + as a type guard. Such a function should use ``TypeIs[...]`` as its return type to alert static type checkers to this intention. - Using ``-> TypeNarrower`` tells the static type checker that for a given + Using ``-> TypeIs`` tells the static type checker that for a given function: 1. The return value is a boolean. @@ -1859,7 +1859,7 @@ def TypeNarrower(self, parameters): For example:: - def is_awaitable(val: object) -> TypeNarrower[Awaitable[Any]]: + def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]: return hasattr(val, '__await__') def f(val: Union[int, Awaitable[int]]) -> int: @@ -1868,36 +1868,36 @@ def f(val: Union[int, Awaitable[int]]) -> int: else: assert_type(val, int) - ``TypeNarrower`` also works with type variables. For more information, see - PEP 742 (Narrowing types with TypeNarrower). + ``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 _TypeNarrowerForm(_ExtensionsSpecialForm, _root=True): + 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,)) - TypeNarrower = _TypeNarrowerForm( - 'TypeNarrower', + TypeIs = _TypeIsForm( + 'TypeIs', doc="""Special typing form used to annotate the return type of a user-defined - type narrower function. ``TypeNarrower`` only accepts a single type argument. + type narrower function. ``TypeIs`` only accepts a single type argument. At runtime, functions marked this way should return a boolean. - ``TypeNarrower`` aims to benefit *type narrowing* -- a technique used by static + ``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 ``TypeNarrower[...]`` as its + as a type guard. Such a function should use ``TypeIs[...]`` as its return type to alert static type checkers to this intention. - Using ``-> TypeNarrower`` tells the static type checker that for a given + Using ``-> TypeIs`` tells the static type checker that for a given function: 1. The return value is a boolean. @@ -1907,7 +1907,7 @@ def __getitem__(self, parameters): For example:: - def is_awaitable(val: object) -> TypeNarrower[Awaitable[Any]]: + def is_awaitable(val: object) -> TypeIs[Awaitable[Any]]: return hasattr(val, '__await__') def f(val: Union[int, Awaitable[int]]) -> int: @@ -1916,8 +1916,8 @@ def f(val: Union[int, Awaitable[int]]) -> int: else: assert_type(val, int) - ``TypeNarrower`` also works with type variables. For more information, see - PEP 742 (Narrowing types with TypeNarrower). + ``TypeIs`` also works with type variables. For more information, see + PEP 742 (Narrowing types with TypeIs). """)