Skip to content

Commit 2faa2a8

Browse files
committed
Add Never and assert_never
Backport of python/cpython#30842, with additional tests from @sobolevn's python/cpython#31222.
1 parent 31c318d commit 2faa2a8

File tree

4 files changed

+210
-21
lines changed

4 files changed

+210
-21
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: 135 additions & 0 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',
@@ -2195,6 +2197,112 @@ def __subclasscheck__(self, cls):
21952197
Self = _Self(_root=True)
21962198

21972199

2200+
if hasattr(typing, "Never"):
2201+
Never = typing.Never
2202+
elif sys.version_info[:2] >= (3, 7):
2203+
# Vendored from cpython typing._SpecialFrom
2204+
class _SpecialForm(typing._Final, _root=True):
2205+
__slots__ = ('_name', '__doc__', '_getitem')
2206+
2207+
def __init__(self, getitem):
2208+
self._getitem = getitem
2209+
self._name = getitem.__name__
2210+
self.__doc__ = getitem.__doc__
2211+
2212+
def __getattr__(self, item):
2213+
if item in {'__name__', '__qualname__'}:
2214+
return self._name
2215+
2216+
raise AttributeError(item)
2217+
2218+
def __mro_entries__(self, bases):
2219+
raise TypeError(f"Cannot subclass {self!r}")
2220+
2221+
def __repr__(self):
2222+
return f'typing_extensions.{self._name}'
2223+
2224+
def __reduce__(self):
2225+
return self._name
2226+
2227+
def __call__(self, *args, **kwds):
2228+
raise TypeError(f"Cannot instantiate {self!r}")
2229+
2230+
def __or__(self, other):
2231+
return typing.Union[self, other]
2232+
2233+
def __ror__(self, other):
2234+
return typing.Union[other, self]
2235+
2236+
def __instancecheck__(self, obj):
2237+
raise TypeError(f"{self} cannot be used with isinstance()")
2238+
2239+
def __subclasscheck__(self, cls):
2240+
raise TypeError(f"{self} cannot be used with issubclass()")
2241+
2242+
@typing._tp_cache
2243+
def __getitem__(self, parameters):
2244+
return self._getitem(self, parameters)
2245+
2246+
@_SpecialForm
2247+
def Never(self, params):
2248+
"""The bottom type, a type that has no members.
2249+
2250+
This can be used to define a function that should never be
2251+
called, or a function that never returns::
2252+
2253+
from typing_extensions import Never
2254+
2255+
def never_call_me(arg: Never) -> None:
2256+
pass
2257+
2258+
def int_or_str(arg: int | str) -> None:
2259+
never_call_me(arg) # type checker error
2260+
match arg:
2261+
case int():
2262+
print("It's an int")
2263+
case str():
2264+
print("It's a str")
2265+
case _:
2266+
never_call_me(arg) # ok, arg is of type Never
2267+
2268+
"""
2269+
2270+
raise TypeError(f"{self} is not subscriptable")
2271+
else:
2272+
class _Never(typing._FinalTypingBase, _root=True):
2273+
"""The bottom type, a type that has no members.
2274+
2275+
This can be used to define a function that should never be
2276+
called, or a function that never returns::
2277+
2278+
from typing_extensions import Never
2279+
2280+
def never_call_me(arg: Never) -> None:
2281+
pass
2282+
2283+
def int_or_str(arg: int | str) -> None:
2284+
never_call_me(arg) # type checker error
2285+
match arg:
2286+
case int():
2287+
print("It's an int")
2288+
case str():
2289+
print("It's a str")
2290+
case _:
2291+
never_call_me(arg) # ok, arg is of type Never
2292+
2293+
"""
2294+
2295+
__slots__ = ()
2296+
2297+
def __instancecheck__(self, obj):
2298+
raise TypeError(f"{self} cannot be used with isinstance().")
2299+
2300+
def __subclasscheck__(self, cls):
2301+
raise TypeError(f"{self} cannot be used with issubclass().")
2302+
2303+
Never = _Never(_root=True)
2304+
2305+
21982306
if hasattr(typing, 'Required'):
21992307
Required = typing.Required
22002308
NotRequired = typing.NotRequired
@@ -2377,6 +2485,33 @@ def reveal_type(__obj: T) -> T:
23772485
return __obj
23782486

23792487

2488+
if hasattr(typing, "assert_never"):
2489+
assert_never = typing.assert_never
2490+
else:
2491+
def assert_never(arg: Never, /) -> Never:
2492+
"""Assert to the type checker that a line of code is unreachable.
2493+
2494+
Example::
2495+
2496+
def int_or_str(arg: int | str) -> None:
2497+
match arg:
2498+
case int():
2499+
print("It's an int")
2500+
case str():
2501+
print("It's a str")
2502+
case _:
2503+
assert_never(arg)
2504+
2505+
If a type checker finds that a call to assert_never() is
2506+
reachable, it will emit an error.
2507+
2508+
At runtime, this throws an exception when called.
2509+
2510+
"""
2511+
raise AssertionError("Expected code to be unreachable")
2512+
2513+
2514+
23802515
if hasattr(typing, 'dataclass_transform'):
23812516
dataclass_transform = typing.dataclass_transform
23822517
else:

0 commit comments

Comments
 (0)