Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not intersect types in isinstance checks if at least one is final. #16330

Merged
merged 1 commit into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -5232,6 +5232,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 @@ -5244,10 +5253,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 @@ -2044,7 +2044,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