Skip to content

Commit 5054b4b

Browse files
Add Never and assert_never (#1060)
Backport of python/cpython#30842, with additional tests from @sobolevn's python/cpython#31222.
1 parent 5a3be63 commit 5054b4b

File tree

4 files changed

+172
-24
lines changed

4 files changed

+172
-24
lines changed

typing_extensions/CHANGELOG

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Release 4.x.x
22

3+
- Add `Never` and `assert_never`. Backport from bpo-46475.
34
- `ParamSpec` args and kwargs are now equal to themselves. Backport from
45
bpo-46676. Patch by Gregory Beauregard (@GBeauregard).
56
- Add `reveal_type`. Backport from bpo-46414.

typing_extensions/README.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ This module currently contains the following:
4343

4444
- In ``typing`` since Python 3.11
4545

46+
- ``assert_never``
47+
- ``Never``
4648
- ``reveal_type``
4749
- ``Self`` (see PEP 673)
4850

typing_extensions/src/test_typing_extensions.py

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from unittest import TestCase, main, skipUnless, skipIf
1313
from test import ann_module, ann_module2, ann_module3
1414
import typing
15-
from typing import TypeVar, Optional, Union
15+
from typing import TypeVar, Optional, Union, Any
1616
from typing import T, KT, VT # Not in __all__.
1717
from typing import Tuple, List, Dict, Iterable, Iterator, Callable
1818
from typing import Generic, NamedTuple
@@ -22,7 +22,7 @@
2222
from typing_extensions import TypeAlias, ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs, TypeGuard
2323
from typing_extensions import Awaitable, AsyncIterator, AsyncContextManager, Required, NotRequired
2424
from typing_extensions import Protocol, runtime, runtime_checkable, Annotated, overload, final, is_typeddict
25-
from typing_extensions import dataclass_transform, reveal_type
25+
from typing_extensions import dataclass_transform, reveal_type, Never, assert_never
2626
try:
2727
from typing_extensions import get_type_hints
2828
except ImportError:
@@ -70,43 +70,94 @@ class Employee:
7070
pass
7171

7272

73-
class NoReturnTests(BaseTestCase):
73+
class BottomTypeTestsMixin:
74+
bottom_type: ClassVar[Any]
7475

75-
def test_noreturn_instance_type_error(self):
76-
with self.assertRaises(TypeError):
77-
isinstance(42, NoReturn)
76+
def test_equality(self):
77+
self.assertEqual(self.bottom_type, self.bottom_type)
78+
self.assertIs(self.bottom_type, self.bottom_type)
79+
self.assertNotEqual(self.bottom_type, None)
7880

79-
def test_noreturn_subclass_type_error_1(self):
80-
with self.assertRaises(TypeError):
81-
issubclass(Employee, NoReturn)
81+
@skipUnless(PEP_560, "Python 3.7+ required")
82+
def test_get_origin(self):
83+
from typing_extensions import get_origin
84+
self.assertIs(get_origin(self.bottom_type), None)
8285

83-
def test_noreturn_subclass_type_error_2(self):
86+
def test_instance_type_error(self):
8487
with self.assertRaises(TypeError):
85-
issubclass(NoReturn, Employee)
88+
isinstance(42, self.bottom_type)
8689

87-
def test_repr(self):
88-
if hasattr(typing, 'NoReturn'):
89-
self.assertEqual(repr(NoReturn), 'typing.NoReturn')
90-
else:
91-
self.assertEqual(repr(NoReturn), 'typing_extensions.NoReturn')
90+
def test_subclass_type_error(self):
91+
with self.assertRaises(TypeError):
92+
issubclass(Employee, self.bottom_type)
93+
with self.assertRaises(TypeError):
94+
issubclass(NoReturn, self.bottom_type)
9295

9396
def test_not_generic(self):
9497
with self.assertRaises(TypeError):
95-
NoReturn[int]
98+
self.bottom_type[int]
9699

97100
def test_cannot_subclass(self):
98101
with self.assertRaises(TypeError):
99-
class A(NoReturn):
102+
class A(self.bottom_type):
100103
pass
101104
with self.assertRaises(TypeError):
102-
class A(type(NoReturn)):
105+
class A(type(self.bottom_type)):
103106
pass
104107

105108
def test_cannot_instantiate(self):
106109
with self.assertRaises(TypeError):
107-
NoReturn()
110+
self.bottom_type()
108111
with self.assertRaises(TypeError):
109-
type(NoReturn)()
112+
type(self.bottom_type)()
113+
114+
115+
class NoReturnTests(BottomTypeTestsMixin, BaseTestCase):
116+
bottom_type = NoReturn
117+
118+
def test_repr(self):
119+
if hasattr(typing, 'NoReturn'):
120+
self.assertEqual(repr(NoReturn), 'typing.NoReturn')
121+
else:
122+
self.assertEqual(repr(NoReturn), 'typing_extensions.NoReturn')
123+
124+
def test_get_type_hints(self):
125+
def some(arg: NoReturn) -> NoReturn: ...
126+
def some_str(arg: 'NoReturn') -> 'typing.NoReturn': ...
127+
128+
expected = {'arg': NoReturn, 'return': NoReturn}
129+
for target in [some, some_str]:
130+
with self.subTest(target=target):
131+
self.assertEqual(gth(target), expected)
132+
133+
def test_not_equality(self):
134+
self.assertNotEqual(NoReturn, Never)
135+
self.assertNotEqual(Never, NoReturn)
136+
137+
138+
class NeverTests(BottomTypeTestsMixin, BaseTestCase):
139+
bottom_type = Never
140+
141+
def test_repr(self):
142+
if hasattr(typing, 'Never'):
143+
self.assertEqual(repr(Never), 'typing.Never')
144+
else:
145+
self.assertEqual(repr(Never), 'typing_extensions.Never')
146+
147+
def test_get_type_hints(self):
148+
def some(arg: Never) -> Never: ...
149+
def some_str(arg: 'Never') -> 'typing_extensions.Never': ...
150+
151+
expected = {'arg': Never, 'return': Never}
152+
for target in [some, some_str]:
153+
with self.subTest(target=target):
154+
self.assertEqual(gth(target), expected)
155+
156+
157+
class AssertNeverTests(BaseTestCase):
158+
def test_exception(self):
159+
with self.assertRaises(AssertionError):
160+
assert_never(None)
110161

111162

112163
class ClassVarTests(BaseTestCase):

typing_extensions/src/typing_extensions.py

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def _check_generic(cls, parameters):
7070

7171
# One-off things.
7272
'Annotated',
73+
'assert_never',
7374
'dataclass_transform',
7475
'final',
7576
'IntVar',
@@ -85,6 +86,7 @@ def _check_generic(cls, parameters):
8586
'TypeAlias',
8687
'TypeGuard',
8788
'TYPE_CHECKING',
89+
'Never',
8890
'NoReturn',
8991
'Required',
9092
'NotRequired',
@@ -2107,9 +2109,8 @@ def __eq__(self, other):
21072109

21082110
TypeGuard = _TypeGuard(_root=True)
21092111

2110-
if hasattr(typing, "Self"):
2111-
Self = typing.Self
2112-
elif sys.version_info[:2] >= (3, 7):
2112+
2113+
if sys.version_info[:2] >= (3, 7):
21132114
# Vendored from cpython typing._SpecialFrom
21142115
class _SpecialForm(typing._Final, _root=True):
21152116
__slots__ = ('_name', '__doc__', '_getitem')
@@ -2153,6 +2154,10 @@ def __subclasscheck__(self, cls):
21532154
def __getitem__(self, parameters):
21542155
return self._getitem(self, parameters)
21552156

2157+
2158+
if hasattr(typing, "Self"):
2159+
Self = typing.Self
2160+
elif sys.version_info[:2] >= (3, 7):
21562161
@_SpecialForm
21572162
def Self(self, params):
21582163
"""Used to spell the type of "self" in classes.
@@ -2195,6 +2200,69 @@ def __subclasscheck__(self, cls):
21952200
Self = _Self(_root=True)
21962201

21972202

2203+
if hasattr(typing, "Never"):
2204+
Never = typing.Never
2205+
elif sys.version_info[:2] >= (3, 7):
2206+
@_SpecialForm
2207+
def Never(self, params):
2208+
"""The bottom type, a type that has no members.
2209+
2210+
This can be used to define a function that should never be
2211+
called, or a function that never returns::
2212+
2213+
from typing_extensions import Never
2214+
2215+
def never_call_me(arg: Never) -> None:
2216+
pass
2217+
2218+
def int_or_str(arg: int | str) -> None:
2219+
never_call_me(arg) # type checker error
2220+
match arg:
2221+
case int():
2222+
print("It's an int")
2223+
case str():
2224+
print("It's a str")
2225+
case _:
2226+
never_call_me(arg) # ok, arg is of type Never
2227+
2228+
"""
2229+
2230+
raise TypeError(f"{self} is not subscriptable")
2231+
else:
2232+
class _Never(typing._FinalTypingBase, _root=True):
2233+
"""The bottom type, a type that has no members.
2234+
2235+
This can be used to define a function that should never be
2236+
called, or a function that never returns::
2237+
2238+
from typing_extensions import Never
2239+
2240+
def never_call_me(arg: Never) -> None:
2241+
pass
2242+
2243+
def int_or_str(arg: int | str) -> None:
2244+
never_call_me(arg) # type checker error
2245+
match arg:
2246+
case int():
2247+
print("It's an int")
2248+
case str():
2249+
print("It's a str")
2250+
case _:
2251+
never_call_me(arg) # ok, arg is of type Never
2252+
2253+
"""
2254+
2255+
__slots__ = ()
2256+
2257+
def __instancecheck__(self, obj):
2258+
raise TypeError(f"{self} cannot be used with isinstance().")
2259+
2260+
def __subclasscheck__(self, cls):
2261+
raise TypeError(f"{self} cannot be used with issubclass().")
2262+
2263+
Never = _Never(_root=True)
2264+
2265+
21982266
if hasattr(typing, 'Required'):
21992267
Required = typing.Required
22002268
NotRequired = typing.NotRequired
@@ -2377,6 +2445,32 @@ def reveal_type(__obj: T) -> T:
23772445
return __obj
23782446

23792447

2448+
if hasattr(typing, "assert_never"):
2449+
assert_never = typing.assert_never
2450+
else:
2451+
def assert_never(__arg: Never) -> Never:
2452+
"""Assert to the type checker that a line of code is unreachable.
2453+
2454+
Example::
2455+
2456+
def int_or_str(arg: int | str) -> None:
2457+
match arg:
2458+
case int():
2459+
print("It's an int")
2460+
case str():
2461+
print("It's a str")
2462+
case _:
2463+
assert_never(arg)
2464+
2465+
If a type checker finds that a call to assert_never() is
2466+
reachable, it will emit an error.
2467+
2468+
At runtime, this throws an exception when called.
2469+
2470+
"""
2471+
raise AssertionError("Expected code to be unreachable")
2472+
2473+
23802474
if hasattr(typing, 'dataclass_transform'):
23812475
dataclass_transform = typing.dataclass_transform
23822476
else:

0 commit comments

Comments
 (0)