From 6fd23ee9d9f2d14d328dcb9cbe340d9bccc174f8 Mon Sep 17 00:00:00 2001 From: Bas van Beek <43369155+BvB93@users.noreply.github.com> Date: Tue, 28 Sep 2021 16:03:03 +0200 Subject: [PATCH 1/5] Allow `__new__` to return `NoReturn` --- mypy/checker.py | 2 +- mypy/typeops.py | 2 +- mypy/types.py | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 2f99b9b4fece7..049d80ec67362 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -1136,7 +1136,7 @@ def check___new___signature(self, fdef: FuncDef, typ: CallableType) -> None: 'but must return a subtype of' ) elif not isinstance(get_proper_type(bound_type.ret_type), - (AnyType, Instance, TupleType)): + (AnyType, Instance, TupleType, UninhabitedType)): self.fail( message_registry.NON_INSTANCE_NEW_TYPE.format( format_type(bound_type.ret_type)), diff --git a/mypy/typeops.py b/mypy/typeops.py index 57fdfeadad9a1..987d929fcedd7 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -106,7 +106,7 @@ def class_callable(init_type: CallableType, info: TypeInfo, type_type: Instance, default_ret_type = fill_typevars(info) explicit_type = init_ret_type if is_new else orig_self_type if ( - isinstance(explicit_type, (Instance, TupleType)) + isinstance(explicit_type, (Instance, TupleType, UninhabitedType)) # Only use the declared return type from __new__ or declared self in __init__ # if it is actually returning a subtype of what we would return otherwise. and is_subtype(explicit_type, default_ret_type, ignore_type_params=True) diff --git a/mypy/types.py b/mypy/types.py index dadbb84fe1c83..e96f2fb6fd79a 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1312,7 +1312,10 @@ def is_kw_arg(self) -> bool: return ARG_STAR2 in self.arg_kinds def is_type_obj(self) -> bool: - return self.fallback.type.is_metaclass() + return ( + self.fallback.type.is_metaclass() + and not isinstance(get_proper_type(self.ret_type), UninhabitedType) + ) def type_object(self) -> mypy.nodes.TypeInfo: assert self.is_type_obj() From a04be8af3a60dc792b441b77adb18edd11b9cdbc Mon Sep 17 00:00:00 2001 From: Bas van Beek <43369155+BvB93@users.noreply.github.com> Date: Tue, 28 Sep 2021 16:03:28 +0200 Subject: [PATCH 2/5] Allow `__init__` to return `NoReturn` --- mypy/checker.py | 9 ++++++--- mypy/typeops.py | 18 +++++++++++++++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 049d80ec67362..6a332b1413540 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -870,9 +870,12 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: Optional[str]) if isinstance(item, FuncDef): fdef = item # Check if __init__ has an invalid, non-None return type. - if (fdef.info and fdef.name in ('__init__', '__init_subclass__') and - not isinstance(get_proper_type(typ.ret_type), NoneType) and - not self.dynamic_funcs[-1]): + if ( + fdef.info + and fdef.name in ('__init__', '__init_subclass__') + and not isinstance(get_proper_type(typ.ret_type), (NoneType, UninhabitedType)) + and not self.dynamic_funcs[-1] + ): self.fail(message_registry.MUST_HAVE_NONE_RETURN_TYPE.format(fdef.name), item) diff --git a/mypy/typeops.py b/mypy/typeops.py index 987d929fcedd7..45a395c0ed27a 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -5,7 +5,7 @@ since these may assume that MROs are ready. """ -from typing import cast, Optional, List, Sequence, Set, Iterable, TypeVar, Dict, Tuple, Any +from typing import cast, Optional, List, Sequence, Set, Iterable, TypeVar, Dict, Tuple, Any, Union from typing_extensions import Type as TypingType import itertools import sys @@ -45,6 +45,19 @@ def tuple_fallback(typ: TupleType) -> Instance: return Instance(info, [join_type_list(typ.items)]) +def get_self_type(func: CallableType, default_self: Union[Instance, TupleType]) -> Optional[Type]: + if isinstance(get_proper_type(func.ret_type), UninhabitedType): + return func.ret_type + elif ( + func.arg_types + and func.arg_types[0] != default_self + and func.arg_kinds[0] == ARG_POS + ): + return func.arg_types[0] + else: + return None + + def type_object_type_from_function(signature: FunctionLike, info: TypeInfo, def_info: TypeInfo, @@ -57,8 +70,7 @@ def type_object_type_from_function(signature: FunctionLike, # classes such as subprocess.Popen. default_self = fill_typevars(info) if not is_new and not info.is_newtype: - orig_self_types = [(it.arg_types[0] if it.arg_types and it.arg_types[0] != default_self - and it.arg_kinds[0] == ARG_POS else None) for it in signature.items] + orig_self_types = [get_self_type(it, default_self) for it in signature.items] else: orig_self_types = [None] * len(signature.items) From e6c8efb0dab1e8a1e3b439cea749627f33967539 Mon Sep 17 00:00:00 2001 From: Bas van Beek <43369155+BvB93@users.noreply.github.com> Date: Tue, 28 Sep 2021 16:48:06 +0200 Subject: [PATCH 3/5] Add `NoReturn` tests for `__new__` and `__init__` --- test-data/unit/check-classes.test | 68 +++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 65b0e8d69cb57..ccd796438bba7 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -7284,3 +7284,71 @@ def meth1(self: Any, y: str) -> str: ... T = TypeVar("T") def meth2(self: Any, y: T) -> T: ... + +[case testNoReturn] +from typing import NoReturn + +class A: + def __new__(cls) -> NoReturn: ... + +class B: + def __init__(self) -> NoReturn: ... + +class C: + def __new__(cls) -> "C": ... + def __init__(self) -> NoReturn: ... + +class D: + def __new__(cls) -> NoReturn: ... + def __init__(self) -> NoReturn: ... + +reveal_type(A()) # N: Revealed type is "" +reveal_type(B()) # N: Revealed type is "" +reveal_type(C()) # N: Revealed type is "" +reveal_type(D()) # N: Revealed type is "" + +[case testNoReturnOverload] +from typing import NoReturn, overload + +class A: + @overload + def __new__(cls) -> NoReturn: ... + @overload + def __new__(cls, a: int) -> "A": ... + def __new__(cls, a: int = ...) -> "A": ... + +class B: + @overload + def __init__(self) -> NoReturn: ... + @overload + def __init__(self, a: int) -> None: ... + def __init__(self, a: int = ...) -> None: ... + +class C: + def __new__(cls, a: int = ...) -> "C": ... + @overload + def __init__(self) -> NoReturn: ... + @overload + def __init__(self, a: int) -> None: ... + def __init__(self, a: int = ...) -> None: ... + +class D: + @overload + def __new__(cls) -> NoReturn: ... + @overload + def __new__(cls, a: int) -> "D": ... + def __new__(cls, a: int = ...) -> "D": ... + @overload + def __init__(self) -> NoReturn: ... + @overload + def __init__(self, a: int) -> None: ... + def __init__(self, a: int = ...) -> None: ... + +reveal_type(A()) # N: Revealed type is "" +reveal_type(A(1)) # N: Revealed type is "__main__.A" +reveal_type(B()) # N: Revealed type is "" +reveal_type(B(1)) # N: Revealed type is "__main__.B" +reveal_type(C()) # N: Revealed type is "" +reveal_type(C(1)) # N: Revealed type is "__main__.C" +reveal_type(D()) # N: Revealed type is "" +reveal_type(D(1)) # N: Revealed type is "__main__.D" From df7b24dc38cfd71da0fa9e0bf7ea949f6bc992e6 Mon Sep 17 00:00:00 2001 From: Bas van Beek <43369155+BvB93@users.noreply.github.com> Date: Tue, 28 Sep 2021 22:10:40 +0200 Subject: [PATCH 4/5] Fix line length issue --- mypy/checker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 6a332b1413540..5697b959bc0e0 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -873,7 +873,8 @@ def check_func_def(self, defn: FuncItem, typ: CallableType, name: Optional[str]) if ( fdef.info and fdef.name in ('__init__', '__init_subclass__') - and not isinstance(get_proper_type(typ.ret_type), (NoneType, UninhabitedType)) + and not isinstance(get_proper_type(typ.ret_type), + (NoneType, UninhabitedType)) and not self.dynamic_funcs[-1] ): self.fail(message_registry.MUST_HAVE_NONE_RETURN_TYPE.format(fdef.name), From 0fdee34abdff533cbdf046860ff2e0ed0d97a07f Mon Sep 17 00:00:00 2001 From: Bas van Beek <43369155+BvB93@users.noreply.github.com> Date: Wed, 29 Sep 2021 12:33:59 +0200 Subject: [PATCH 5/5] TST: Provide more descriptive test-case names Co-Authored-By: Nikita Sobolev --- test-data/unit/check-classes.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index ccd796438bba7..ca349a8d9378e 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -7285,7 +7285,7 @@ def meth1(self: Any, y: str) -> str: ... T = TypeVar("T") def meth2(self: Any, y: T) -> T: ... -[case testNoReturn] +[case testNewAndInitNoReturn] from typing import NoReturn class A: @@ -7307,7 +7307,7 @@ reveal_type(B()) # N: Revealed type is "" reveal_type(C()) # N: Revealed type is "" reveal_type(D()) # N: Revealed type is "" -[case testNoReturnOverload] +[case testOverloadedNewAndInitNoReturn] from typing import NoReturn, overload class A: