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

@final class without __bool__ cannot have falsey instances #16566

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 16 additions & 9 deletions mypy/typeops.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,15 +569,15 @@ def _remove_redundant_union_items(items: list[Type], keep_erased: bool) -> list[
return items


def _get_type_special_method_bool_ret_type(t: Type) -> Type | None:
def _get_type_method_ret_type(t: Type, *, name: str) -> Type | None:
t = get_proper_type(t)

if isinstance(t, Instance):
bool_method = t.type.get("__bool__")
if bool_method:
callee = get_proper_type(bool_method.type)
if isinstance(callee, CallableType):
return callee.ret_type
sym = t.type.get(name)
if sym:
sym_type = get_proper_type(sym.type)
if isinstance(sym_type, CallableType):
return sym_type.ret_type

return None

Expand All @@ -600,7 +600,9 @@ def true_only(t: Type) -> ProperType:
can_be_true_items = [item for item in new_items if item.can_be_true]
return make_simplified_union(can_be_true_items, line=t.line, column=t.column)
else:
ret_type = _get_type_special_method_bool_ret_type(t)
ret_type = _get_type_method_ret_type(t, name="__bool__") or _get_type_method_ret_type(
t, name="__len__"
)

if ret_type and not ret_type.can_be_true:
return UninhabitedType(line=t.line, column=t.column)
Expand Down Expand Up @@ -633,9 +635,14 @@ def false_only(t: Type) -> ProperType:
can_be_false_items = [item for item in new_items if item.can_be_false]
return make_simplified_union(can_be_false_items, line=t.line, column=t.column)
else:
ret_type = _get_type_special_method_bool_ret_type(t)
ret_type = _get_type_method_ret_type(t, name="__bool__") or _get_type_method_ret_type(
t, name="__len__"
)

if ret_type and not ret_type.can_be_false:
if ret_type:
if not ret_type.can_be_false:
return UninhabitedType(line=t.line)
elif isinstance(t, Instance) and t.type.is_final:
return UninhabitedType(line=t.line)

new_t = copy_type(t)
Expand Down
44 changes: 44 additions & 0 deletions test-data/unit/check-final.test
Original file line number Diff line number Diff line change
Expand Up @@ -1130,3 +1130,47 @@ class Child(Parent):
__foo: Final[int] = 1
@final
def __bar(self) -> None: ...

[case testFinalWithoutBool]
from typing_extensions import final, Literal

class A:
pass

@final
class B:
pass

@final
class C:
def __len__(self) -> Literal[1]: return 1

reveal_type(A() and 42) # N: Revealed type is "Union[__main__.A, Literal[42]?]"
reveal_type(B() and 42) # N: Revealed type is "Literal[42]?"
reveal_type(C() and 42) # N: Revealed type is "Literal[42]?"

[builtins fixtures/bool.pyi]

[case testFinalWithoutBoolButWithLen]
from typing_extensions import final, Literal

# Per Python data model, __len__ is called if __bool__ does not exist.In a @final class,
# __bool__ would not exist.
ikonst marked this conversation as resolved.
Show resolved Hide resolved

@final
class A:
def __len__(self) -> int: ...

@final
class B:
def __len__(self) -> Literal[1]: return 1

@final
class C:
def __len__(self) -> Literal[0]: return 0

reveal_type(A() and 42) # N: Revealed type is "Union[__main__.A, Literal[42]?]"
reveal_type(B() and 42) # N: Revealed type is "Literal[42]?"
reveal_type(C() and 42) # N: Revealed type is "__main__.C"

[builtins fixtures/bool.pyi]