Skip to content

Commit 7c33e7c

Browse files
authored
@Final class without __bool__ cannot have falsey instances (#16566)
Once class C is final, we know that a derived class won't add a `__bool__` or a `__len__` so if they're missing, we can assume every instance of C to be truthy. Relates to #16565
1 parent c224da5 commit 7c33e7c

File tree

2 files changed

+60
-9
lines changed

2 files changed

+60
-9
lines changed

mypy/typeops.py

+16-9
Original file line numberDiff line numberDiff line change
@@ -569,15 +569,15 @@ def _remove_redundant_union_items(items: list[Type], keep_erased: bool) -> list[
569569
return items
570570

571571

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

575575
if isinstance(t, Instance):
576-
bool_method = t.type.get("__bool__")
577-
if bool_method:
578-
callee = get_proper_type(bool_method.type)
579-
if isinstance(callee, CallableType):
580-
return callee.ret_type
576+
sym = t.type.get(name)
577+
if sym:
578+
sym_type = get_proper_type(sym.type)
579+
if isinstance(sym_type, CallableType):
580+
return sym_type.ret_type
581581

582582
return None
583583

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

605607
if ret_type and not ret_type.can_be_true:
606608
return UninhabitedType(line=t.line, column=t.column)
@@ -633,9 +635,14 @@ def false_only(t: Type) -> ProperType:
633635
can_be_false_items = [item for item in new_items if item.can_be_false]
634636
return make_simplified_union(can_be_false_items, line=t.line, column=t.column)
635637
else:
636-
ret_type = _get_type_special_method_bool_ret_type(t)
638+
ret_type = _get_type_method_ret_type(t, name="__bool__") or _get_type_method_ret_type(
639+
t, name="__len__"
640+
)
637641

638-
if ret_type and not ret_type.can_be_false:
642+
if ret_type:
643+
if not ret_type.can_be_false:
644+
return UninhabitedType(line=t.line)
645+
elif isinstance(t, Instance) and t.type.is_final:
639646
return UninhabitedType(line=t.line)
640647

641648
new_t = copy_type(t)

test-data/unit/check-final.test

+44
Original file line numberDiff line numberDiff line change
@@ -1130,3 +1130,47 @@ class Child(Parent):
11301130
__foo: Final[int] = 1
11311131
@final
11321132
def __bar(self) -> None: ...
1133+
1134+
[case testFinalWithoutBool]
1135+
from typing_extensions import final, Literal
1136+
1137+
class A:
1138+
pass
1139+
1140+
@final
1141+
class B:
1142+
pass
1143+
1144+
@final
1145+
class C:
1146+
def __len__(self) -> Literal[1]: return 1
1147+
1148+
reveal_type(A() and 42) # N: Revealed type is "Union[__main__.A, Literal[42]?]"
1149+
reveal_type(B() and 42) # N: Revealed type is "Literal[42]?"
1150+
reveal_type(C() and 42) # N: Revealed type is "Literal[42]?"
1151+
1152+
[builtins fixtures/bool.pyi]
1153+
1154+
[case testFinalWithoutBoolButWithLen]
1155+
from typing_extensions import final, Literal
1156+
1157+
# Per Python data model, __len__ is called if __bool__ does not exist.
1158+
# In a @final class, __bool__ would not exist.
1159+
1160+
@final
1161+
class A:
1162+
def __len__(self) -> int: ...
1163+
1164+
@final
1165+
class B:
1166+
def __len__(self) -> Literal[1]: return 1
1167+
1168+
@final
1169+
class C:
1170+
def __len__(self) -> Literal[0]: return 0
1171+
1172+
reveal_type(A() and 42) # N: Revealed type is "Union[__main__.A, Literal[42]?]"
1173+
reveal_type(B() and 42) # N: Revealed type is "Literal[42]?"
1174+
reveal_type(C() and 42) # N: Revealed type is "__main__.C"
1175+
1176+
[builtins fixtures/bool.pyi]

0 commit comments

Comments
 (0)