Skip to content

Commit c33fe16

Browse files
Implement TypeGuard (PEP 649) (#803)
This should be the last missing piece for a 3.10-compatible release. The implementation was mostly inspired by that of Final, with an additional special case for 3.9.
1 parent 4ba98e8 commit c33fe16

File tree

3 files changed

+337
-2
lines changed

3 files changed

+337
-2
lines changed

typing_extensions/README.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ All Python versions:
4646
- ``overload`` (note that older versions of ``typing`` only let you use ``overload`` in stubs)
4747
- ``OrderedDict``
4848
- ``Protocol`` (except on Python 3.5.0)
49-
- ``runtime`` (except on Python 3.5.0)
49+
- ``runtime_checkable`` (except on Python 3.5.0)
5050
- ``Text``
5151
- ``Type``
5252
- ``TypedDict``
@@ -61,6 +61,7 @@ Python 3.4+ only:
6161
- ``Concatenate``
6262
- ``ParamSpecArgs``
6363
- ``ParamSpecKwargs``
64+
- ``TypeGuard``
6465

6566
Python 3.5+ only:
6667
-----------------

typing_extensions/src_py3/test_typing_extensions.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from typing import Generic
1414
from typing import no_type_check
1515
from typing_extensions import NoReturn, ClassVar, Final, IntVar, Literal, Type, NewType, TypedDict
16-
from typing_extensions import TypeAlias, ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs
16+
from typing_extensions import TypeAlias, ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs, TypeGuard
1717

1818
try:
1919
from typing_extensions import Protocol, runtime, runtime_checkable
@@ -2108,6 +2108,51 @@ def test_eq(self):
21082108
self.assertNotEqual(C1, C3)
21092109

21102110

2111+
class TypeGuardTests(BaseTestCase):
2112+
def test_basics(self):
2113+
TypeGuard[int] # OK
2114+
self.assertEqual(TypeGuard[int], TypeGuard[int])
2115+
2116+
def foo(arg) -> TypeGuard[int]: ...
2117+
self.assertEqual(gth(foo), {'return': TypeGuard[int]})
2118+
2119+
def test_repr(self):
2120+
if hasattr(typing, 'TypeGuard'):
2121+
mod_name = 'typing'
2122+
else:
2123+
mod_name = 'typing_extensions'
2124+
self.assertEqual(repr(TypeGuard), '{}.TypeGuard'.format(mod_name))
2125+
cv = TypeGuard[int]
2126+
self.assertEqual(repr(cv), '{}.TypeGuard[int]'.format(mod_name))
2127+
cv = TypeGuard[Employee]
2128+
self.assertEqual(repr(cv), '{}.TypeGuard[{}.Employee]'.format(mod_name, __name__))
2129+
cv = TypeGuard[Tuple[int]]
2130+
self.assertEqual(repr(cv), '{}.TypeGuard[typing.Tuple[int]]'.format(mod_name))
2131+
2132+
@skipUnless(SUBCLASS_CHECK_FORBIDDEN, "Behavior added in typing 3.5.3")
2133+
def test_cannot_subclass(self):
2134+
with self.assertRaises(TypeError):
2135+
class C(type(TypeGuard)):
2136+
pass
2137+
with self.assertRaises(TypeError):
2138+
class C(type(TypeGuard[int])):
2139+
pass
2140+
2141+
def test_cannot_init(self):
2142+
with self.assertRaises(TypeError):
2143+
TypeGuard()
2144+
with self.assertRaises(TypeError):
2145+
type(TypeGuard)()
2146+
with self.assertRaises(TypeError):
2147+
type(TypeGuard[Optional[int]])()
2148+
2149+
def test_no_isinstance(self):
2150+
with self.assertRaises(TypeError):
2151+
isinstance(1, TypeGuard[int])
2152+
with self.assertRaises(TypeError):
2153+
issubclass(int, TypeGuard)
2154+
2155+
21112156
class AllTests(BaseTestCase):
21122157

21132158
def test_typing_extensions_includes_standard(self):

typing_extensions/src_py3/typing_extensions.py

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ def _check_methods_in_mro(C, *methods):
150150
'overload',
151151
'Text',
152152
'TypeAlias',
153+
'TypeGuard',
153154
'TYPE_CHECKING',
154155
]
155156

@@ -2514,3 +2515,291 @@ class Concatenate(metaclass=_ConcatenateAliasMeta, _root=True):
25142515
See PEP 612 for detailed information.
25152516
"""
25162517
__slots__ = ()
2518+
2519+
if hasattr(typing, 'TypeGuard'):
2520+
TypeGuard = typing.TypeGuard
2521+
elif sys.version_info[:2] >= (3, 9):
2522+
class _TypeGuardForm(typing._SpecialForm, _root=True):
2523+
def __repr__(self):
2524+
return 'typing_extensions.' + self._name
2525+
2526+
@_TypeGuardForm
2527+
def TypeGuard(self, parameters):
2528+
"""Special typing form used to annotate the return type of a user-defined
2529+
type guard function. ``TypeGuard`` only accepts a single type argument.
2530+
At runtime, functions marked this way should return a boolean.
2531+
2532+
``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static
2533+
type checkers to determine a more precise type of an expression within a
2534+
program's code flow. Usually type narrowing is done by analyzing
2535+
conditional code flow and applying the narrowing to a block of code. The
2536+
conditional expression here is sometimes referred to as a "type guard".
2537+
2538+
Sometimes it would be convenient to use a user-defined boolean function
2539+
as a type guard. Such a function should use ``TypeGuard[...]`` as its
2540+
return type to alert static type checkers to this intention.
2541+
2542+
Using ``-> TypeGuard`` tells the static type checker that for a given
2543+
function:
2544+
2545+
1. The return value is a boolean.
2546+
2. If the return value is ``True``, the type of its argument
2547+
is the type inside ``TypeGuard``.
2548+
2549+
For example::
2550+
2551+
def is_str(val: Union[str, float]):
2552+
# "isinstance" type guard
2553+
if isinstance(val, str):
2554+
# Type of ``val`` is narrowed to ``str``
2555+
...
2556+
else:
2557+
# Else, type of ``val`` is narrowed to ``float``.
2558+
...
2559+
2560+
Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower
2561+
form of ``TypeA`` (it can even be a wider form) and this may lead to
2562+
type-unsafe results. The main reason is to allow for things like
2563+
narrowing ``List[object]`` to ``List[str]`` even though the latter is not
2564+
a subtype of the former, since ``List`` is invariant. The responsibility of
2565+
writing type-safe type guards is left to the user.
2566+
2567+
``TypeGuard`` also works with type variables. For more information, see
2568+
PEP 647 (User-Defined Type Guards).
2569+
"""
2570+
item = typing._type_check(parameters, '{} accepts only single type.'.format(self))
2571+
return _GenericAlias(self, (item,))
2572+
2573+
elif sys.version_info[:2] >= (3, 7):
2574+
class _TypeGuardForm(typing._SpecialForm, _root=True):
2575+
2576+
def __repr__(self):
2577+
return 'typing_extensions.' + self._name
2578+
2579+
def __getitem__(self, parameters):
2580+
item = typing._type_check(parameters,
2581+
'{} accepts only a single type'.format(self._name))
2582+
return _GenericAlias(self, (item,))
2583+
2584+
TypeGuard = _TypeGuardForm(
2585+
'TypeGuard',
2586+
doc="""Special typing form used to annotate the return type of a user-defined
2587+
type guard function. ``TypeGuard`` only accepts a single type argument.
2588+
At runtime, functions marked this way should return a boolean.
2589+
2590+
``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static
2591+
type checkers to determine a more precise type of an expression within a
2592+
program's code flow. Usually type narrowing is done by analyzing
2593+
conditional code flow and applying the narrowing to a block of code. The
2594+
conditional expression here is sometimes referred to as a "type guard".
2595+
2596+
Sometimes it would be convenient to use a user-defined boolean function
2597+
as a type guard. Such a function should use ``TypeGuard[...]`` as its
2598+
return type to alert static type checkers to this intention.
2599+
2600+
Using ``-> TypeGuard`` tells the static type checker that for a given
2601+
function:
2602+
2603+
1. The return value is a boolean.
2604+
2. If the return value is ``True``, the type of its argument
2605+
is the type inside ``TypeGuard``.
2606+
2607+
For example::
2608+
2609+
def is_str(val: Union[str, float]):
2610+
# "isinstance" type guard
2611+
if isinstance(val, str):
2612+
# Type of ``val`` is narrowed to ``str``
2613+
...
2614+
else:
2615+
# Else, type of ``val`` is narrowed to ``float``.
2616+
...
2617+
2618+
Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower
2619+
form of ``TypeA`` (it can even be a wider form) and this may lead to
2620+
type-unsafe results. The main reason is to allow for things like
2621+
narrowing ``List[object]`` to ``List[str]`` even though the latter is not
2622+
a subtype of the former, since ``List`` is invariant. The responsibility of
2623+
writing type-safe type guards is left to the user.
2624+
2625+
``TypeGuard`` also works with type variables. For more information, see
2626+
PEP 647 (User-Defined Type Guards).
2627+
""")
2628+
elif hasattr(typing, '_FinalTypingBase'):
2629+
class _TypeGuard(typing._FinalTypingBase, _root=True):
2630+
"""Special typing form used to annotate the return type of a user-defined
2631+
type guard function. ``TypeGuard`` only accepts a single type argument.
2632+
At runtime, functions marked this way should return a boolean.
2633+
2634+
``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static
2635+
type checkers to determine a more precise type of an expression within a
2636+
program's code flow. Usually type narrowing is done by analyzing
2637+
conditional code flow and applying the narrowing to a block of code. The
2638+
conditional expression here is sometimes referred to as a "type guard".
2639+
2640+
Sometimes it would be convenient to use a user-defined boolean function
2641+
as a type guard. Such a function should use ``TypeGuard[...]`` as its
2642+
return type to alert static type checkers to this intention.
2643+
2644+
Using ``-> TypeGuard`` tells the static type checker that for a given
2645+
function:
2646+
2647+
1. The return value is a boolean.
2648+
2. If the return value is ``True``, the type of its argument
2649+
is the type inside ``TypeGuard``.
2650+
2651+
For example::
2652+
2653+
def is_str(val: Union[str, float]):
2654+
# "isinstance" type guard
2655+
if isinstance(val, str):
2656+
# Type of ``val`` is narrowed to ``str``
2657+
...
2658+
else:
2659+
# Else, type of ``val`` is narrowed to ``float``.
2660+
...
2661+
2662+
Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower
2663+
form of ``TypeA`` (it can even be a wider form) and this may lead to
2664+
type-unsafe results. The main reason is to allow for things like
2665+
narrowing ``List[object]`` to ``List[str]`` even though the latter is not
2666+
a subtype of the former, since ``List`` is invariant. The responsibility of
2667+
writing type-safe type guards is left to the user.
2668+
2669+
``TypeGuard`` also works with type variables. For more information, see
2670+
PEP 647 (User-Defined Type Guards).
2671+
"""
2672+
2673+
__slots__ = ('__type__',)
2674+
2675+
def __init__(self, tp=None, **kwds):
2676+
self.__type__ = tp
2677+
2678+
def __getitem__(self, item):
2679+
cls = type(self)
2680+
if self.__type__ is None:
2681+
return cls(typing._type_check(item,
2682+
'{} accepts only a single type.'.format(cls.__name__[1:])),
2683+
_root=True)
2684+
raise TypeError('{} cannot be further subscripted'
2685+
.format(cls.__name__[1:]))
2686+
2687+
def _eval_type(self, globalns, localns):
2688+
new_tp = typing._eval_type(self.__type__, globalns, localns)
2689+
if new_tp == self.__type__:
2690+
return self
2691+
return type(self)(new_tp, _root=True)
2692+
2693+
def __repr__(self):
2694+
r = super().__repr__()
2695+
if self.__type__ is not None:
2696+
r += '[{}]'.format(typing._type_repr(self.__type__))
2697+
return r
2698+
2699+
def __hash__(self):
2700+
return hash((type(self).__name__, self.__type__))
2701+
2702+
def __eq__(self, other):
2703+
if not isinstance(other, _TypeGuard):
2704+
return NotImplemented
2705+
if self.__type__ is not None:
2706+
return self.__type__ == other.__type__
2707+
return self is other
2708+
2709+
TypeGuard = _TypeGuard(_root=True)
2710+
else:
2711+
class _TypeGuardMeta(typing.TypingMeta):
2712+
"""Metaclass for TypeGuard"""
2713+
2714+
def __new__(cls, name, bases, namespace, tp=None, _root=False):
2715+
self = super().__new__(cls, name, bases, namespace, _root=_root)
2716+
if tp is not None:
2717+
self.__type__ = tp
2718+
return self
2719+
2720+
def __instancecheck__(self, obj):
2721+
raise TypeError("TypeGuard cannot be used with isinstance().")
2722+
2723+
def __subclasscheck__(self, cls):
2724+
raise TypeError("TypeGuard cannot be used with issubclass().")
2725+
2726+
def __getitem__(self, item):
2727+
cls = type(self)
2728+
if self.__type__ is not None:
2729+
raise TypeError('{} cannot be further subscripted'
2730+
.format(cls.__name__[1:]))
2731+
2732+
param = typing._type_check(
2733+
item,
2734+
'{} accepts only single type.'.format(cls.__name__[1:]))
2735+
return cls(self.__name__, self.__bases__,
2736+
dict(self.__dict__), tp=param, _root=True)
2737+
2738+
def _eval_type(self, globalns, localns):
2739+
new_tp = typing._eval_type(self.__type__, globalns, localns)
2740+
if new_tp == self.__type__:
2741+
return self
2742+
return type(self)(self.__name__, self.__bases__,
2743+
dict(self.__dict__), tp=self.__type__,
2744+
_root=True)
2745+
2746+
def __repr__(self):
2747+
r = super().__repr__()
2748+
if self.__type__ is not None:
2749+
r += '[{}]'.format(typing._type_repr(self.__type__))
2750+
return r
2751+
2752+
def __hash__(self):
2753+
return hash((type(self).__name__, self.__type__))
2754+
2755+
def __eq__(self, other):
2756+
if not hasattr(other, "__type__"):
2757+
return NotImplemented
2758+
if self.__type__ is not None:
2759+
return self.__type__ == other.__type__
2760+
return self is other
2761+
2762+
class TypeGuard(typing.Final, metaclass=_TypeGuardMeta, _root=True):
2763+
"""Special typing form used to annotate the return type of a user-defined
2764+
type guard function. ``TypeGuard`` only accepts a single type argument.
2765+
At runtime, functions marked this way should return a boolean.
2766+
2767+
``TypeGuard`` aims to benefit *type narrowing* -- a technique used by static
2768+
type checkers to determine a more precise type of an expression within a
2769+
program's code flow. Usually type narrowing is done by analyzing
2770+
conditional code flow and applying the narrowing to a block of code. The
2771+
conditional expression here is sometimes referred to as a "type guard".
2772+
2773+
Sometimes it would be convenient to use a user-defined boolean function
2774+
as a type guard. Such a function should use ``TypeGuard[...]`` as its
2775+
return type to alert static type checkers to this intention.
2776+
2777+
Using ``-> TypeGuard`` tells the static type checker that for a given
2778+
function:
2779+
2780+
1. The return value is a boolean.
2781+
2. If the return value is ``True``, the type of its argument
2782+
is the type inside ``TypeGuard``.
2783+
2784+
For example::
2785+
2786+
def is_str(val: Union[str, float]):
2787+
# "isinstance" type guard
2788+
if isinstance(val, str):
2789+
# Type of ``val`` is narrowed to ``str``
2790+
...
2791+
else:
2792+
# Else, type of ``val`` is narrowed to ``float``.
2793+
...
2794+
2795+
Strict type narrowing is not enforced -- ``TypeB`` need not be a narrower
2796+
form of ``TypeA`` (it can even be a wider form) and this may lead to
2797+
type-unsafe results. The main reason is to allow for things like
2798+
narrowing ``List[object]`` to ``List[str]`` even though the latter is not
2799+
a subtype of the former, since ``List`` is invariant. The responsibility of
2800+
writing type-safe type guards is left to the user.
2801+
2802+
``TypeGuard`` also works with type variables. For more information, see
2803+
PEP 647 (User-Defined Type Guards).
2804+
"""
2805+
__type__ = None

0 commit comments

Comments
 (0)