Skip to content

Commit

Permalink
@Final class without __bool__ cannot have falsey instances (#16566)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ikonst committed Dec 4, 2023
1 parent c224da5 commit 7c33e7c
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 9 deletions.
25 changes: 16 additions & 9 deletions mypy/typeops.py
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
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.

@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]

0 comments on commit 7c33e7c

Please sign in to comment.