From 5b96ac5204aaf64a614163ce87cba8034c528c71 Mon Sep 17 00:00:00 2001 From: Jannic Warken Date: Fri, 21 Jan 2022 20:08:49 +0100 Subject: [PATCH] Improve consistency of NoReturn --- mypy/checkexpr.py | 2 -- mypy/test/testtypes.py | 6 ++++++ mypy/test/typefixture.py | 1 + mypy/typeops.py | 12 +++++++++--- mypy/types.py | 19 ++++++++++++++++++- test-data/unit/check-flags.test | 2 +- test-data/unit/check-unions.test | 25 +++++++++++++++++++++++-- 7 files changed, 58 insertions(+), 9 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 9bf3ec3a4456..66d8fa4b7696 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -378,8 +378,6 @@ def visit_call_expr_inner(self, e: CallExpr, allow_none_return: bool = False) -> if isinstance(e.callee, MemberExpr) and e.callee.name == 'format': self.check_str_format_call(e) ret_type = get_proper_type(ret_type) - if isinstance(ret_type, UnionType): - ret_type = make_simplified_union(ret_type.items) if isinstance(ret_type, UninhabitedType) and not ret_type.ambiguous: self.chk.binder.unreachable() # Warn on calls to functions that always return None. The check diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index 0d37fe795bbc..f8bd24f38c7c 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -480,6 +480,12 @@ def test_simplified_union(self) -> None: self.assert_simplified_union([fx.lit1, fx.lit2_inst], UnionType([fx.lit1, fx.lit2_inst])) self.assert_simplified_union([fx.lit1, fx.lit3_inst], UnionType([fx.lit1, fx.lit3_inst])) + self.assert_simplified_union([fx.noreturn], fx.noreturn) + self.assert_simplified_union([fx.noreturn, fx.noreturn], fx.noreturn) + self.assert_simplified_union([fx.a, fx.noreturn], UnionType([fx.a, fx.noreturn])) + self.assert_simplified_union([fx.lit1_inst, fx.noreturn], + UnionType([fx.lit1_inst, fx.noreturn])) + def assert_simplified_union(self, original: List[Type], union: Type) -> None: assert_equal(make_simplified_union(original), union) assert_equal(make_simplified_union(list(reversed(original))), union) diff --git a/mypy/test/typefixture.py b/mypy/test/typefixture.py index 1a5dd8164136..04e0d4e15c7f 100644 --- a/mypy/test/typefixture.py +++ b/mypy/test/typefixture.py @@ -46,6 +46,7 @@ def make_type_var(name: str, id: int, values: List[Type], upper_bound: Type, self.anyt = AnyType(TypeOfAny.special_form) self.nonet = NoneType() self.uninhabited = UninhabitedType() + self.noreturn = UninhabitedType(is_noreturn=True) # Abstract class TypeInfos diff --git a/mypy/typeops.py b/mypy/typeops.py index 88d4bc744bdf..388a65b34a57 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -11,10 +11,12 @@ import sys from mypy.types import ( - TupleType, Instance, FunctionLike, Type, CallableType, TypeVarLikeType, Overloaded, + TupleType, Instance, FunctionLike, Type, CallableType, TypeVarLikeType, + Overloaded, TypeVarType, UninhabitedType, FormalArgument, UnionType, NoneType, - AnyType, TypeOfAny, TypeType, ProperType, LiteralType, get_proper_type, get_proper_types, - copy_type, TypeAliasType, TypeQuery, ParamSpecType + AnyType, TypeOfAny, TypeType, ProperType, LiteralType, get_proper_type, + get_proper_types, + copy_type, TypeAliasType, TypeQuery, ParamSpecType, is_proper_noreturn_type ) from mypy.nodes import ( FuncBase, FuncItem, FuncDef, OverloadedFuncDef, TypeInfo, ARG_STAR, ARG_STAR2, ARG_POS, @@ -320,6 +322,9 @@ def make_simplified_union(items: Sequence[Type], * [int, int] -> int * [int, Any] -> Union[int, Any] (Any types are not simplified away!) * [Any, Any] -> Any + * [NoReturn] -> NoReturn + * [int, NoReturn] -> Union[int, NoReturn] (NoReturn types are not simplified + if other types are present) Note: This must NOT be used during semantic analysis, since TypeInfos may not be fully initialized. @@ -377,6 +382,7 @@ def make_simplified_union(items: Sequence[Type], and not is_simple_literal(tj) and is_proper_subtype(tj, item, keep_erased_types=keep_erased) and is_redundant_literal_instance(item, tj) # XXX? + and (not is_proper_noreturn_type(tj) or is_proper_noreturn_type(item)) ): # We found a redundant item in the union. removed.add(j) diff --git a/mypy/types.py b/mypy/types.py index d3987897995e..28f2f40b470f 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1952,7 +1952,8 @@ class UnionType(ProperType): def __init__(self, items: Sequence[Type], line: int = -1, column: int = -1, is_evaluated: bool = True, uses_pep604_syntax: bool = False) -> None: super().__init__(line, column) - self.items = flatten_nested_unions(items) + items = flatten_nested_unions(items) + self.items = reduce_proper_noreturn_types(items) self.can_be_true = any(item.can_be_true for item in items) self.can_be_false = any(item.can_be_false for item in items) # is_evaluated should be set to false for type comments and string literals @@ -2575,6 +2576,17 @@ def flatten_nested_unions(types: Iterable[Type], return flat_items +def reduce_proper_noreturn_types(types: Iterable[Type]) -> List[Type]: + """Remove one or multiple NoReturn types if there is at least one other type. + + Assumes the union was flattened (except for possible union type aliases). The + implementation CANNOT handle type aliases, because they might not yet be expanded. + """ + filtered: List[Type] = [tp for tp in types if not is_proper_noreturn_type(tp)] + # If nothing is left, we know there was at least one NoReturn. + return filtered if filtered else [UninhabitedType(is_noreturn=True)] + + def union_items(typ: Type) -> List[ProperType]: """Return the flattened items of a union type. @@ -2611,6 +2623,11 @@ def is_optional(t: Type) -> bool: for e in t.items) +def is_proper_noreturn_type(t: Type) -> bool: + return isinstance(t, ProperType) and \ + isinstance(t, UninhabitedType) and t.is_noreturn + + def remove_optional(typ: Type) -> Type: typ = get_proper_type(typ) if isinstance(typ, UnionType): diff --git a/test-data/unit/check-flags.test b/test-data/unit/check-flags.test index 8f72a82b0760..d2a5898f2b63 100644 --- a/test-data/unit/check-flags.test +++ b/test-data/unit/check-flags.test @@ -385,7 +385,7 @@ from mypy_extensions import NoReturn def no_return() -> NoReturn: pass def f() -> int: return 0 -reveal_type(f() or no_return()) # N: Revealed type is "builtins.int" +reveal_type(f() or no_return()) # N: Revealed type is "Union[builtins.int]" [builtins fixtures/dict.pyi] [case testNoReturnVariable] diff --git a/test-data/unit/check-unions.test b/test-data/unit/check-unions.test index 6966af289f28..33cea84c0734 100644 --- a/test-data/unit/check-unions.test +++ b/test-data/unit/check-unions.test @@ -146,8 +146,29 @@ f('') # E: Argument 1 to "f" has incompatible type "str"; expected "Optional[int [case testUnionWithNoReturn] from typing import Union, NoReturn -def f() -> Union[int, NoReturn]: ... -reveal_type(f()) # N: Revealed type is "builtins.int" + +def func() -> Union[int, NoReturn]: ... +reveal_type(func()) # N: Revealed type is "Union[builtins.int]" + +a: Union[NoReturn] +reveal_type(a) # N: Revealed type is "" + +b: Union[NoReturn, NoReturn] +reveal_type(b) # N: Revealed type is "Union[]" + +c: Union[str, NoReturn] +reveal_type(c) # N: Revealed type is "Union[builtins.str]" + +A = Union[str, NoReturn] +d: A +reveal_type(d) # N: Revealed type is "Union[builtins.str]" + +e: Union[str, Union[NoReturn, int]] +reveal_type(e) # N: Revealed type is "Union[builtins.str, builtins.int]" + +B = Union[str, int] +f: Union[NoReturn, B] +reveal_type(f) # N: Revealed type is "Union[Union[builtins.str, builtins.int]]" [case testUnionSimplificationGenericFunction] from typing import TypeVar, Union, List