From f89e57208b18b811389f33de8d20ec07db4e3225 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Wed, 22 Dec 2021 23:14:30 +0300 Subject: [PATCH 1/2] NamedTuple now narrows to bool correctly, when `__bool__` is defined, refs #11819 --- mypy/types.py | 27 ++++++++++++++++--- test-data/unit/check-namedtuple.test | 39 ++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/mypy/types.py b/mypy/types.py index 14eefea7dd81..196362c48ff4 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1500,12 +1500,31 @@ class TupleType(ProperType): def __init__(self, items: List[Type], fallback: Instance, line: int = -1, column: int = -1, implicit: bool = False) -> None: - super().__init__(line, column) - self.items = items self.partial_fallback = fallback + self.items = items self.implicit = implicit - self.can_be_true = len(self.items) > 0 - self.can_be_false = len(self.items) == 0 + super().__init__(line, column) + + def can_be_true_default(self) -> bool: + if self.can_be_any_bool(): + # Corner case: it is a `NamedTuple` with `__bool__` method defined. + # It can be anything: both `True` and `False`. + return True + return self.length() > 0 + + def can_be_false_default(self) -> bool: + if self.can_be_any_bool(): + # Corner case: it is a `NamedTuple` with `__bool__` method defined. + # It can be anything: both `True` and `False`. + return True + return self.length() == 0 + + def can_be_any_bool(self) -> bool: + return ( + self.partial_fallback.type + and self.partial_fallback.type.fullname != 'builtins.tuple' + and self.partial_fallback.type.names.get('__bool__') + ) def length(self) -> int: return len(self.items) diff --git a/test-data/unit/check-namedtuple.test b/test-data/unit/check-namedtuple.test index 440884333c69..bf4d24a54552 100644 --- a/test-data/unit/check-namedtuple.test +++ b/test-data/unit/check-namedtuple.test @@ -1081,3 +1081,42 @@ t: T y: List[T] = [t] [builtins fixtures/tuple.pyi] [typing fixtures/typing-namedtuple.pyi] + +[case testNamedTupleWithBoolNarrowsToBool] +# flags: --warn-unreachable +from typing import NamedTuple + +class C(NamedTuple): + x: int + + def __bool__(self) -> bool: + pass + +def foo(c: C) -> None: + if c: + reveal_type(c) # N: Revealed type is "Tuple[builtins.int, fallback=__main__.C]" + else: + reveal_type(c) # N: Revealed type is "Tuple[builtins.int, fallback=__main__.C]" + +def bar(c: C) -> None: + if not c: + reveal_type(c) # N: Revealed type is "Tuple[builtins.int, fallback=__main__.C]" + else: + reveal_type(c) # N: Revealed type is "Tuple[builtins.int, fallback=__main__.C]" + +class C1(NamedTuple): + x: int + +def foo1(c: C1) -> None: + if c: + reveal_type(c) # N: Revealed type is "Tuple[builtins.int, fallback=__main__.C1]" + else: + c # E: Statement is unreachable + +def bar1(c: C1) -> None: + if not c: + c # E: Statement is unreachable + else: + reveal_type(c) # N: Revealed type is "Tuple[builtins.int, fallback=__main__.C1]" +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-namedtuple.pyi] From 6bada5a4a3d1b5279133cb25691bdf21c58779a7 Mon Sep 17 00:00:00 2001 From: Nikita Sobolev Date: Wed, 22 Dec 2021 23:31:48 +0300 Subject: [PATCH 2/2] Update types.py --- mypy/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/types.py b/mypy/types.py index 196362c48ff4..53aee1a0fe65 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -1520,7 +1520,7 @@ def can_be_false_default(self) -> bool: return self.length() == 0 def can_be_any_bool(self) -> bool: - return ( + return bool( self.partial_fallback.type and self.partial_fallback.type.fullname != 'builtins.tuple' and self.partial_fallback.type.names.get('__bool__')