aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJelle Zijlstra <jelle.zijlstra@gmail.com>2022-02-10 17:48:13 -0800
committerGitHub <noreply@github.com>2022-02-10 17:48:13 -0800
commita53957cad88444d9a33e5906cef31c39fb0b919a (patch)
tree102be7c77ae5f26383bf1cf4ff2a588f1a475f99
parentc1db137f573d4b47bc5a141fb8f048f2d7c75fcc (diff)
downloadtyping-a53957cad88444d9a33e5906cef31c39fb0b919a.tar.gz
Add Never and assert_never (#1060)
Backport of python/cpython#30842, with additional tests from @sobolevn's python/cpython#31222.
-rw-r--r--typing_extensions/CHANGELOG1
-rw-r--r--typing_extensions/README.rst2
-rw-r--r--typing_extensions/src/test_typing_extensions.py93
-rw-r--r--typing_extensions/src/typing_extensions.py100
4 files changed, 172 insertions, 24 deletions
diff --git a/typing_extensions/CHANGELOG b/typing_extensions/CHANGELOG
index c96bd34..092e04a 100644
--- a/typing_extensions/CHANGELOG
+++ b/typing_extensions/CHANGELOG
@@ -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.
diff --git a/typing_extensions/README.rst b/typing_extensions/README.rst
index 961bdf9..a83ed3c 100644
--- a/typing_extensions/README.rst
+++ b/typing_extensions/README.rst
@@ -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)
diff --git a/typing_extensions/src/test_typing_extensions.py b/typing_extensions/src/test_typing_extensions.py
index 4e1d95f..9111688 100644
--- a/typing_extensions/src/test_typing_extensions.py
+++ b/typing_extensions/src/test_typing_extensions.py
@@ -12,7 +12,7 @@ import types
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
@@ -22,7 +22,7 @@ from typing_extensions import NoReturn, ClassVar, Final, IntVar, Literal, Type,
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:
@@ -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):
diff --git a/typing_extensions/src/typing_extensions.py b/typing_extensions/src/typing_extensions.py
index 27eaff0..b67efd0 100644
--- a/typing_extensions/src/typing_extensions.py
+++ b/typing_extensions/src/typing_extensions.py
@@ -70,6 +70,7 @@ __all__ = [
# One-off things.
'Annotated',
+ 'assert_never',
'dataclass_transform',
'final',
'IntVar',
@@ -85,6 +86,7 @@ __all__ = [
'TypeAlias',
'TypeGuard',
'TYPE_CHECKING',
+ 'Never',
'NoReturn',
'Required',
'NotRequired',
@@ -2107,9 +2109,8 @@ else:
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')
@@ -2153,6 +2154,10 @@ elif sys.version_info[:2] >= (3, 7):
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.
@@ -2195,6 +2200,69 @@ else:
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
@@ -2377,6 +2445,32 @@ else:
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: