Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.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).
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:: 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.
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, 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
Expand Down Expand Up @@ -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:
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',
'TypeNarrower',
'TYPE_CHECKING',
'Never',
'NoReturn',
Expand Down Expand Up @@ -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):
Expand Down