Skip to content

Commit

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

Fixes python#15148

I think it also fixes the [initial
bug](python#12163 (comment))
reported in python#12163 (this is why I added a TypeVar test case) but not
[this
bug](python#12163 (comment))
reported later in the same issue.
  • Loading branch information
tyralla authored 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.