gh-117516: Implement typing.TypeIs (#117517)

See PEP 742.

Co-authored-by: Carl Meyer <carl@oddbird.net>
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
This commit is contained in:
Jelle Zijlstra 2024-04-09 06:50:37 -04:00 committed by GitHub
parent 57183241af
commit f2132fcd2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 235 additions and 38 deletions

View file

@ -153,6 +153,7 @@ __all__ = [
'TYPE_CHECKING',
'TypeAlias',
'TypeGuard',
'TypeIs',
'TypeAliasType',
'Unpack',
]
@ -818,28 +819,31 @@ def Concatenate(self, parameters):
@_SpecialForm
def TypeGuard(self, parameters):
"""Special typing construct for marking user-defined type guard functions.
"""Special typing construct for marking user-defined type predicate functions.
``TypeGuard`` can be used to annotate the return type of a user-defined
type guard function. ``TypeGuard`` only accepts a single type argument.
type predicate function. ``TypeGuard`` only accepts a single type argument.
At runtime, functions marked this way should return a boolean.
``TypeGuard`` 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".
conditional expression here is sometimes referred to as a "type predicate".
Sometimes it would be convenient to use a user-defined boolean function
as a type guard. Such a function should use ``TypeGuard[...]`` as its
return type to alert static type checkers to this intention.
as a type predicate. Such a function should use ``TypeGuard[...]`` or
``TypeIs[...]`` as its return type to alert static type checkers to
this intention. ``TypeGuard`` should be used over ``TypeIs`` when narrowing
from an incompatible type (e.g., ``list[object]`` to ``list[int]``) or when
the function does not return ``True`` for all instances of the narrowed type.
Using ``-> TypeGuard`` tells the static type checker that for a given
function:
Using ``-> TypeGuard[NarrowedType]`` 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 type inside ``TypeGuard``.
is ``NarrowedType``.
For example::
@ -860,7 +864,7 @@ def TypeGuard(self, parameters):
type-unsafe results. The main reason is to allow for things like
narrowing ``list[object]`` to ``list[str]`` even though the latter is not
a subtype of the former, since ``list`` is invariant. The responsibility of
writing type-safe type guards is left to the user.
writing type-safe type predicates is left to the user.
``TypeGuard`` also works with type variables. For more information, see
PEP 647 (User-Defined Type Guards).
@ -869,6 +873,75 @@ def TypeGuard(self, parameters):
return _GenericAlias(self, (item,))
@_SpecialForm
def TypeIs(self, parameters):
"""Special typing construct for marking user-defined type predicate functions.
``TypeIs`` can be used to annotate the return type of a user-defined
type predicate function. ``TypeIs`` only accepts a single type argument.
At runtime, functions marked this way should return a boolean and accept
at least one argument.
``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 predicate".
Sometimes it would be convenient to use a user-defined boolean function
as a type predicate. Such a function should use ``TypeIs[...]`` or
``TypeGuard[...]`` as its return type to alert static type checkers to
this intention. ``TypeIs`` usually has more intuitive behavior than
``TypeGuard``, but it cannot be used when the input and output types
are incompatible (e.g., ``list[object]`` to ``list[int]``) or when the
function does not return ``True`` for all instances of the narrowed type.
Using ``-> TypeIs[NarrowedType]`` 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 argument's original type and
``NarrowedType``.
3. If the return value is ``False``, the type of its argument
is narrowed to exclude ``NarrowedType``.
For example::
from typing import assert_type, final, TypeIs
class Parent: pass
class Child(Parent): pass
@final
class Unrelated: pass
def is_parent(val: object) -> TypeIs[Parent]:
return isinstance(val, Parent)
def run(arg: Child | Unrelated):
if is_parent(arg):
# Type of ``arg`` is narrowed to the intersection
# of ``Parent`` and ``Child``, which is equivalent to
# ``Child``.
assert_type(arg, Child)
else:
# Type of ``arg`` is narrowed to exclude ``Parent``,
# so only ``Unrelated`` is left.
assert_type(arg, Unrelated)
The type inside ``TypeIs`` must be consistent with the type of the
function's argument; if it is not, static type checkers will raise
an error. An incorrectly written ``TypeIs`` function can lead to
unsound behavior in the type system; it is the user's responsibility
to write such functions in a type-safe manner.
``TypeIs`` also works with type variables. For more information, see
PEP 742 (Narrowing types with ``TypeIs``).
"""
item = _type_check(parameters, f'{self} accepts only single type.')
return _GenericAlias(self, (item,))
class ForwardRef(_Final, _root=True):
"""Internal wrapper to hold a forward reference."""
@ -1241,11 +1314,12 @@ class _GenericAlias(_BaseGenericAlias, _root=True):
# A = Callable[[], None] # _CallableGenericAlias
# B = Callable[[T], None] # _CallableGenericAlias
# C = B[int] # _CallableGenericAlias
# * Parameterized `Final`, `ClassVar` and `TypeGuard`:
# * Parameterized `Final`, `ClassVar`, `TypeGuard`, and `TypeIs`:
# # All _GenericAlias
# Final[int]
# ClassVar[float]
# TypeVar[bool]
# TypeGuard[bool]
# TypeIs[range]
def __init__(self, origin, args, *, inst=True, name=None):
super().__init__(origin, inst=inst, name=name)