Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Never and assert_never #1060

Merged
merged 4 commits into from
Feb 11, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions typing_extensions/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 typing_extensions/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 typing_extensions/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
134 changes: 134 additions & 0 deletions typing_extensions/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 @@ -2195,6 +2197,112 @@ def __subclasscheck__(self, cls):
Self = _Self(_root=True)


if hasattr(typing, "Never"):
Never = typing.Never
elif sys.version_info[:2] >= (3, 7):
# Vendored from cpython typing._SpecialFrom
class _SpecialForm(typing._Final, _root=True):
JelleZijlstra marked this conversation as resolved.
Show resolved Hide resolved
__slots__ = ('_name', '__doc__', '_getitem')

def __init__(self, getitem):
self._getitem = getitem
self._name = getitem.__name__
self.__doc__ = getitem.__doc__

def __getattr__(self, item):
if item in {'__name__', '__qualname__'}:
return self._name

raise AttributeError(item)

def __mro_entries__(self, bases):
raise TypeError(f"Cannot subclass {self!r}")

def __repr__(self):
return f'typing_extensions.{self._name}'

def __reduce__(self):
return self._name

def __call__(self, *args, **kwds):
raise TypeError(f"Cannot instantiate {self!r}")

def __or__(self, other):
return typing.Union[self, other]

def __ror__(self, other):
return typing.Union[other, self]

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()")

@typing._tp_cache
def __getitem__(self, parameters):
return self._getitem(self, parameters)

@_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 +2485,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