Skip to content

Commit

Permalink
Add Never and assert_never (#1060)
Browse files Browse the repository at this point in the history
Backport of python/cpython#30842, with additional tests from @sobolevn's python/cpython#31222.
  • Loading branch information
JelleZijlstra authored Feb 11, 2022
1 parent 4c81bd1 commit 039b433
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 24 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Release 4.x.x

- Add `Never` and `assert_never`. Backport from bpo-46475.
- `ParamSpec` args and kwargs are now equal to themselves. Backport from
bpo-46676. Patch by Gregory Beauregard (@GBeauregard).
- Add `reveal_type`. Backport from bpo-46414.
Expand Down
2 changes: 2 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ This module currently contains the following:

- In ``typing`` since Python 3.11

- ``assert_never``
- ``Never``
- ``reveal_type``
- ``Self`` (see PEP 673)

Expand Down
93 changes: 72 additions & 21 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from unittest import TestCase, main, skipUnless, skipIf
from test import ann_module, ann_module2, ann_module3
import typing
from typing import TypeVar, Optional, Union
from typing import TypeVar, Optional, Union, Any
from typing import T, KT, VT # Not in __all__.
from typing import Tuple, List, Dict, Iterable, Iterator, Callable
from typing import Generic, NamedTuple
Expand All @@ -22,7 +22,7 @@
from typing_extensions import TypeAlias, ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs, TypeGuard
from typing_extensions import Awaitable, AsyncIterator, AsyncContextManager, Required, NotRequired
from typing_extensions import Protocol, runtime, runtime_checkable, Annotated, overload, final, is_typeddict
from typing_extensions import dataclass_transform, reveal_type
from typing_extensions import dataclass_transform, reveal_type, Never, assert_never
try:
from typing_extensions import get_type_hints
except ImportError:
Expand Down Expand Up @@ -70,43 +70,94 @@ class Employee:
pass


class NoReturnTests(BaseTestCase):
class BottomTypeTestsMixin:
bottom_type: ClassVar[Any]

def test_noreturn_instance_type_error(self):
with self.assertRaises(TypeError):
isinstance(42, NoReturn)
def test_equality(self):
self.assertEqual(self.bottom_type, self.bottom_type)
self.assertIs(self.bottom_type, self.bottom_type)
self.assertNotEqual(self.bottom_type, None)

def test_noreturn_subclass_type_error_1(self):
with self.assertRaises(TypeError):
issubclass(Employee, NoReturn)
@skipUnless(PEP_560, "Python 3.7+ required")
def test_get_origin(self):
from typing_extensions import get_origin
self.assertIs(get_origin(self.bottom_type), None)

def test_noreturn_subclass_type_error_2(self):
def test_instance_type_error(self):
with self.assertRaises(TypeError):
issubclass(NoReturn, Employee)
isinstance(42, self.bottom_type)

def test_repr(self):
if hasattr(typing, 'NoReturn'):
self.assertEqual(repr(NoReturn), 'typing.NoReturn')
else:
self.assertEqual(repr(NoReturn), 'typing_extensions.NoReturn')
def test_subclass_type_error(self):
with self.assertRaises(TypeError):
issubclass(Employee, self.bottom_type)
with self.assertRaises(TypeError):
issubclass(NoReturn, self.bottom_type)

def test_not_generic(self):
with self.assertRaises(TypeError):
NoReturn[int]
self.bottom_type[int]

def test_cannot_subclass(self):
with self.assertRaises(TypeError):
class A(NoReturn):
class A(self.bottom_type):
pass
with self.assertRaises(TypeError):
class A(type(NoReturn)):
class A(type(self.bottom_type)):
pass

def test_cannot_instantiate(self):
with self.assertRaises(TypeError):
NoReturn()
self.bottom_type()
with self.assertRaises(TypeError):
type(NoReturn)()
type(self.bottom_type)()


class NoReturnTests(BottomTypeTestsMixin, BaseTestCase):
bottom_type = NoReturn

def test_repr(self):
if hasattr(typing, 'NoReturn'):
self.assertEqual(repr(NoReturn), 'typing.NoReturn')
else:
self.assertEqual(repr(NoReturn), 'typing_extensions.NoReturn')

def test_get_type_hints(self):
def some(arg: NoReturn) -> NoReturn: ...
def some_str(arg: 'NoReturn') -> 'typing.NoReturn': ...

expected = {'arg': NoReturn, 'return': NoReturn}
for target in [some, some_str]:
with self.subTest(target=target):
self.assertEqual(gth(target), expected)

def test_not_equality(self):
self.assertNotEqual(NoReturn, Never)
self.assertNotEqual(Never, NoReturn)


class NeverTests(BottomTypeTestsMixin, BaseTestCase):
bottom_type = Never

def test_repr(self):
if hasattr(typing, 'Never'):
self.assertEqual(repr(Never), 'typing.Never')
else:
self.assertEqual(repr(Never), 'typing_extensions.Never')

def test_get_type_hints(self):
def some(arg: Never) -> Never: ...
def some_str(arg: 'Never') -> 'typing_extensions.Never': ...

expected = {'arg': Never, 'return': Never}
for target in [some, some_str]:
with self.subTest(target=target):
self.assertEqual(gth(target), expected)


class AssertNeverTests(BaseTestCase):
def test_exception(self):
with self.assertRaises(AssertionError):
assert_never(None)


class ClassVarTests(BaseTestCase):
Expand Down
100 changes: 97 additions & 3 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def _check_generic(cls, parameters):

# One-off things.
'Annotated',
'assert_never',
'dataclass_transform',
'final',
'IntVar',
Expand All @@ -85,6 +86,7 @@ def _check_generic(cls, parameters):
'TypeAlias',
'TypeGuard',
'TYPE_CHECKING',
'Never',
'NoReturn',
'Required',
'NotRequired',
Expand Down Expand Up @@ -2107,9 +2109,8 @@ def __eq__(self, other):

TypeGuard = _TypeGuard(_root=True)

if hasattr(typing, "Self"):
Self = typing.Self
elif sys.version_info[:2] >= (3, 7):

if sys.version_info[:2] >= (3, 7):
# Vendored from cpython typing._SpecialFrom
class _SpecialForm(typing._Final, _root=True):
__slots__ = ('_name', '__doc__', '_getitem')
Expand Down Expand Up @@ -2153,6 +2154,10 @@ def __subclasscheck__(self, cls):
def __getitem__(self, parameters):
return self._getitem(self, parameters)


if hasattr(typing, "Self"):
Self = typing.Self
elif sys.version_info[:2] >= (3, 7):
@_SpecialForm
def Self(self, params):
"""Used to spell the type of "self" in classes.
Expand Down Expand Up @@ -2195,6 +2200,69 @@ def __subclasscheck__(self, cls):
Self = _Self(_root=True)


if hasattr(typing, "Never"):
Never = typing.Never
elif sys.version_info[:2] >= (3, 7):
@_SpecialForm
def Never(self, params):
"""The bottom type, a type that has no members.
This can be used to define a function that should never be
called, or a function that never returns::
from typing_extensions import Never
def never_call_me(arg: Never) -> None:
pass
def int_or_str(arg: int | str) -> None:
never_call_me(arg) # type checker error
match arg:
case int():
print("It's an int")
case str():
print("It's a str")
case _:
never_call_me(arg) # ok, arg is of type Never
"""

raise TypeError(f"{self} is not subscriptable")
else:
class _Never(typing._FinalTypingBase, _root=True):
"""The bottom type, a type that has no members.
This can be used to define a function that should never be
called, or a function that never returns::
from typing_extensions import Never
def never_call_me(arg: Never) -> None:
pass
def int_or_str(arg: int | str) -> None:
never_call_me(arg) # type checker error
match arg:
case int():
print("It's an int")
case str():
print("It's a str")
case _:
never_call_me(arg) # ok, arg is of type Never
"""

__slots__ = ()

def __instancecheck__(self, obj):
raise TypeError(f"{self} cannot be used with isinstance().")

def __subclasscheck__(self, cls):
raise TypeError(f"{self} cannot be used with issubclass().")

Never = _Never(_root=True)


if hasattr(typing, 'Required'):
Required = typing.Required
NotRequired = typing.NotRequired
Expand Down Expand Up @@ -2377,6 +2445,32 @@ def reveal_type(__obj: T) -> T:
return __obj


if hasattr(typing, "assert_never"):
assert_never = typing.assert_never
else:
def assert_never(__arg: Never) -> Never:
"""Assert to the type checker that a line of code is unreachable.
Example::
def int_or_str(arg: int | str) -> None:
match arg:
case int():
print("It's an int")
case str():
print("It's a str")
case _:
assert_never(arg)
If a type checker finds that a call to assert_never() is
reachable, it will emit an error.
At runtime, this throws an exception when called.
"""
raise AssertionError("Expected code to be unreachable")


if hasattr(typing, 'dataclass_transform'):
dataclass_transform = typing.dataclass_transform
else:
Expand Down

0 comments on commit 039b433

Please sign in to comment.