From c224da5c7c414f92ded4b7816d16d5dd4ed32193 Mon Sep 17 00:00:00 2001 From: Christoph Tyralla Date: Mon, 4 Dec 2023 08:13:58 +0100 Subject: [PATCH] Do not intersect types in isinstance checks if at least one is final (#16330) Fixes #15148 I think it also fixes the [initial bug](https://github.com/python/mypy/issues/12163#issue-1131262225) reported in #12163 (this is why I added a TypeVar test case) but not [this bug](https://github.com/python/mypy/issues/12163#issuecomment-1035865305) reported later in the same issue. --- mypy/checker.py | 13 +++- mypy/messages.py | 2 +- test-data/unit/check-narrowing.test | 99 +++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 7c6f59fafdc8..979a55b223c9 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -5254,6 +5254,15 @@ def _make_fake_typeinfo_and_full_name( pretty_names_list = pretty_seq( format_type_distinctly(*base_classes, options=self.options, bare=True), "and" ) + + new_errors = [] + for base in base_classes: + if base.type.is_final: + new_errors.append((pretty_names_list, f'"{base.type.name}" is final')) + if new_errors: + errors.extend(new_errors) + return None + try: info, full_name = _make_fake_typeinfo_and_full_name(base_classes, curr_module) with self.msg.filter_errors() as local_errors: @@ -5266,10 +5275,10 @@ def _make_fake_typeinfo_and_full_name( self.check_multiple_inheritance(info) info.is_intersection = True except MroError: - errors.append((pretty_names_list, "inconsistent method resolution order")) + errors.append((pretty_names_list, "would have inconsistent method resolution order")) return None if local_errors.has_new_errors(): - errors.append((pretty_names_list, "incompatible method signatures")) + errors.append((pretty_names_list, "would have incompatible method signatures")) return None curr_module.names[full_name] = SymbolTableNode(GDEF, info) diff --git a/mypy/messages.py b/mypy/messages.py index ddb048444695..069c4d51e281 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -2051,7 +2051,7 @@ def redundant_expr(self, description: str, truthiness: bool, context: Context) - def impossible_intersection( self, formatted_base_class_list: str, reason: str, context: Context ) -> None: - template = "Subclass of {} cannot exist: would have {}" + template = "Subclass of {} cannot exist: {}" self.fail( template.format(formatted_base_class_list, reason), context, code=codes.UNREACHABLE ) diff --git a/test-data/unit/check-narrowing.test b/test-data/unit/check-narrowing.test index d0ad1367aca0..a2859dfffa3a 100644 --- a/test-data/unit/check-narrowing.test +++ b/test-data/unit/check-narrowing.test @@ -1020,6 +1020,105 @@ else: reveal_type(true_or_false) # N: Revealed type is "Literal[False]" [builtins fixtures/primitives.pyi] + +[case testNarrowingIsInstanceFinalSubclass] +# flags: --warn-unreachable + +from typing import final + +class N: ... +@final +class F1: ... +@final +class F2: ... + +n: N +f1: F1 + +if isinstance(f1, F1): + reveal_type(f1) # N: Revealed type is "__main__.F1" +else: + reveal_type(f1) # E: Statement is unreachable + +if isinstance(n, F1): # E: Subclass of "N" and "F1" cannot exist: "F1" is final + reveal_type(n) # E: Statement is unreachable +else: + reveal_type(n) # N: Revealed type is "__main__.N" + +if isinstance(f1, N): # E: Subclass of "F1" and "N" cannot exist: "F1" is final + reveal_type(f1) # E: Statement is unreachable +else: + reveal_type(f1) # N: Revealed type is "__main__.F1" + +if isinstance(f1, F2): # E: Subclass of "F1" and "F2" cannot exist: "F1" is final \ + # E: Subclass of "F1" and "F2" cannot exist: "F2" is final + reveal_type(f1) # E: Statement is unreachable +else: + reveal_type(f1) # N: Revealed type is "__main__.F1" +[builtins fixtures/isinstance.pyi] + + +[case testNarrowingIsInstanceFinalSubclassWithUnions] +# flags: --warn-unreachable + +from typing import final, Union + +class N: ... +@final +class F1: ... +@final +class F2: ... + +n_f1: Union[N, F1] +n_f2: Union[N, F2] +f1_f2: Union[F1, F2] + +if isinstance(n_f1, F1): + reveal_type(n_f1) # N: Revealed type is "__main__.F1" +else: + reveal_type(n_f1) # N: Revealed type is "__main__.N" + +if isinstance(n_f2, F1): # E: Subclass of "N" and "F1" cannot exist: "F1" is final \ + # E: Subclass of "F2" and "F1" cannot exist: "F2" is final \ + # E: Subclass of "F2" and "F1" cannot exist: "F1" is final + reveal_type(n_f2) # E: Statement is unreachable +else: + reveal_type(n_f2) # N: Revealed type is "Union[__main__.N, __main__.F2]" + +if isinstance(f1_f2, F1): + reveal_type(f1_f2) # N: Revealed type is "__main__.F1" +else: + reveal_type(f1_f2) # N: Revealed type is "__main__.F2" +[builtins fixtures/isinstance.pyi] + + +[case testNarrowingIsSubclassFinalSubclassWithTypeVar] +# flags: --warn-unreachable + +from typing import final, Type, TypeVar + +@final +class A: ... +@final +class B: ... + +T = TypeVar("T", A, B) + +def f(cls: Type[T]) -> T: + if issubclass(cls, A): + reveal_type(cls) # N: Revealed type is "Type[__main__.A]" + x: bool + if x: + return A() + else: + return B() # E: Incompatible return value type (got "B", expected "A") + assert False + +reveal_type(f(A)) # N: Revealed type is "__main__.A" +reveal_type(f(B)) # N: Revealed type is "__main__.B" +[builtins fixtures/isinstance.pyi] + + [case testNarrowingLiteralIdentityCheck] from typing import Union from typing_extensions import Literal