Skip to content

Commit

Permalink
Do not intersect types in isinstance checks if at least one is final (#…
Browse files Browse the repository at this point in the history
…16330)

Fixes #15148

I think it also fixes the [initial
bug](#12163 (comment))
reported in #12163 (this is why I added a TypeVar test case) but not
[this
bug](#12163 (comment))
reported later in the same issue.
  • Loading branch information
tyralla committed Dec 4, 2023
1 parent d54cc35 commit c224da5
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 3 deletions.
13 changes: 11 additions & 2 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion mypy/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
99 changes: 99 additions & 0 deletions test-data/unit/check-narrowing.test
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit c224da5

Please sign in to comment.