From b20b2624b746b9fc732292bbe8b4d062e9b7d0f7 Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Sat, 25 Nov 2023 23:58:30 -0500 Subject: [PATCH 1/5] @final class without __bool__ cannot have falsey instances --- mypy/typeops.py | 5 ++++- test-data/unit/check-final.test | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/mypy/typeops.py b/mypy/typeops.py index e92fad0e872c..feae3c12fe12 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -635,7 +635,10 @@ def false_only(t: Type) -> ProperType: else: ret_type = _get_type_special_method_bool_ret_type(t) - 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) diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index a2fd64386707..efa95565a6f7 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -1130,3 +1130,42 @@ 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-xfail] +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 + +reveal_type(A() and 42) # N: Revealed type is "Union[__main__.A, Literal[42]?]" +reveal_type(B() and 42) # N: Revealed type is "__main__.A" + +[builtins fixtures/bool.pyi] From bcb225da37dab2a4e45358da7d5b9ecbe4a4202b Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Sun, 26 Nov 2023 00:34:39 -0500 Subject: [PATCH 2/5] also check __len__ --- mypy/typeops.py | 16 ++++++++++------ test-data/unit/check-final.test | 9 +++++++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/mypy/typeops.py b/mypy/typeops.py index feae3c12fe12..7d2a913ec21a 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -569,13 +569,13 @@ 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_special_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) + method = t.type.get(name) + if method: + callee = get_proper_type(method.type) if isinstance(callee, CallableType): return callee.ret_type @@ -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_special_method_ret_type( + t, name="__bool__" + ) or _get_type_special_method_ret_type(t, name="__len__") if ret_type and not ret_type.can_be_true: return UninhabitedType(line=t.line, column=t.column) @@ -633,7 +635,9 @@ 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_special_method_ret_type( + t, name="__bool__" + ) or _get_type_special_method_ret_type(t, name="__len__") if ret_type: if not ret_type.can_be_false: diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index efa95565a6f7..0dc03b11d2c2 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -1151,7 +1151,7 @@ reveal_type(C() and 42) # N: Revealed type is "Literal[42]?" [builtins fixtures/bool.pyi] -[case testFinalWithoutBoolButWithLen-xfail] +[case testFinalWithoutBoolButWithLen] from typing_extensions import final, Literal # Per Python data model, __len__ is called if __bool__ does not exist.In a @final class, @@ -1165,7 +1165,12 @@ class A: 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 "__main__.A" +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] From b23314f797464e3bcc98ae62125e177185fa2d14 Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Mon, 27 Nov 2023 10:48:02 -0500 Subject: [PATCH 3/5] more precise naming --- mypy/typeops.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/mypy/typeops.py b/mypy/typeops.py index 7d2a913ec21a..ea446b092232 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -569,15 +569,15 @@ def _remove_redundant_union_items(items: list[Type], keep_erased: bool) -> list[ return items -def _get_type_special_method_ret_type(t: Type, *, name: str) -> Type | None: +def _get_type_method_ret_type(t: Type, *, name: str) -> Type | None: t = get_proper_type(t) if isinstance(t, Instance): - method = t.type.get(name) - if method: - callee = get_proper_type(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 @@ -600,9 +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_ret_type( + ret_type = _get_type_method_ret_type( t, name="__bool__" - ) or _get_type_special_method_ret_type(t, name="__len__") + ) 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) @@ -635,9 +635,9 @@ 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_ret_type( + ret_type = _get_type_method_ret_type( t, name="__bool__" - ) or _get_type_special_method_ret_type(t, name="__len__") + ) or _get_type_method_ret_type(t, name="__len__") if ret_type: if not ret_type.can_be_false: From a1f3d11cc1d3ae2e0d9df627870dc0cfbd1386a9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 Nov 2023 15:48:24 +0000 Subject: [PATCH 4/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mypy/typeops.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mypy/typeops.py b/mypy/typeops.py index ea446b092232..2bf8ffbf47ab 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -600,9 +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_method_ret_type( - t, name="__bool__" - ) or _get_type_method_ret_type(t, name="__len__") + 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) @@ -635,9 +635,9 @@ 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_method_ret_type( - t, name="__bool__" - ) or _get_type_method_ret_type(t, name="__len__") + ret_type = _get_type_method_ret_type(t, name="__bool__") or _get_type_method_ret_type( + t, name="__len__" + ) if ret_type: if not ret_type.can_be_false: From 94c9327178b4ad963d47c267ac45ebf32c660ab3 Mon Sep 17 00:00:00 2001 From: Ilya Priven Date: Mon, 27 Nov 2023 11:45:15 -0500 Subject: [PATCH 5/5] Update comment Co-authored-by: Alex Waygood --- test-data/unit/check-final.test | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-data/unit/check-final.test b/test-data/unit/check-final.test index 0dc03b11d2c2..b1378a47b1b1 100644 --- a/test-data/unit/check-final.test +++ b/test-data/unit/check-final.test @@ -1154,8 +1154,8 @@ reveal_type(C() and 42) # N: Revealed type is "Literal[42]?" [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. +# Per Python data model, __len__ is called if __bool__ does not exist. +# In a @final class, __bool__ would not exist. @final class A: