From 1bc2b9da5de75ad8b1dc75dcdcadc579db214c54 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Sat, 29 May 2021 00:23:43 -0400 Subject: [PATCH 01/31] wip wip: always-true --- mypy/types.py | 8 ++++++++ mypy/typeshed/stdlib/datetime.pyi | 1 + test-data/unit/fixtures/dict.pyi | 1 + 3 files changed, 10 insertions(+) diff --git a/mypy/types.py b/mypy/types.py index 7aa83a75f431b..b8264f239ec6a 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -842,6 +842,14 @@ def __init__(self, typ: mypy.nodes.TypeInfo, args: Sequence[Type], # Literal context. self.last_known_value = last_known_value + if ( + self.type and + not self.type.has_readable_member('__bool__') and + not self.type.has_readable_member('__len__') + ): + self.can_be_true = True + self.can_be_false = False + def accept(self, visitor: 'TypeVisitor[T]') -> T: return visitor.visit_instance(self) diff --git a/mypy/typeshed/stdlib/datetime.pyi b/mypy/typeshed/stdlib/datetime.pyi index e82b269235714..8462e80e2a598 100644 --- a/mypy/typeshed/stdlib/datetime.pyi +++ b/mypy/typeshed/stdlib/datetime.pyi @@ -180,6 +180,7 @@ class timedelta(SupportsAbs[timedelta]): def __ge__(self, other: timedelta) -> bool: ... def __gt__(self, other: timedelta) -> bool: ... def __hash__(self) -> int: ... + def __bool__(self) -> bool: ... class datetime(date): min: ClassVar[datetime] diff --git a/test-data/unit/fixtures/dict.pyi b/test-data/unit/fixtures/dict.pyi index ab8127badd4c3..c165fbbeedfcb 100644 --- a/test-data/unit/fixtures/dict.pyi +++ b/test-data/unit/fixtures/dict.pyi @@ -33,6 +33,7 @@ class dict(Mapping[KT, VT]): class int: # for convenience def __add__(self, x: int) -> int: pass + def __bool__(self) -> bool: pass class str: pass # for keyword argument key type class unicode: pass # needed for py2 docstrings From b3b301f79af0905985508d96b68f4ab99c8322ca Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Fri, 18 Jun 2021 01:23:31 -0400 Subject: [PATCH 02/31] fix fixtures and tests --- mypy/test/typefixture.py | 14 ++++++++++++-- test-data/unit/check-dynamic-typing.test | 5 +++-- test-data/unit/check-enum.test | 6 +++--- test-data/unit/check-expressions.test | 9 ++++++--- test-data/unit/check-inference-context.test | 6 ++++-- test-data/unit/check-literal.test | 2 +- test-data/unit/check-typevar-values.test | 3 ++- test-data/unit/fixtures/bool.pyi | 6 ++++-- test-data/unit/fixtures/isinstancelist.pyi | 2 ++ test-data/unit/fixtures/list.pyi | 4 +++- test-data/unit/fixtures/ops.pyi | 4 +++- test-data/unit/fixtures/primitives.pyi | 1 + test-data/unit/lib-stub/builtins.pyi | 7 +++++-- test-data/unit/lib-stub/enum.pyi | 1 + test-data/unit/lib-stub/typing.pyi | 1 + 15 files changed, 51 insertions(+), 20 deletions(-) diff --git a/mypy/test/typefixture.py b/mypy/test/typefixture.py index 3debe85aa22c9..e1b243daae47a 100644 --- a/mypy/test/typefixture.py +++ b/mypy/test/typefixture.py @@ -5,13 +5,14 @@ from typing import List, Optional, Tuple +from mypy.semanal_shared import set_callable_name from mypy.types import ( Type, TypeVarType, AnyType, NoneType, Instance, CallableType, TypeVarDef, TypeType, UninhabitedType, TypeOfAny, TypeAliasType, UnionType, LiteralType ) from mypy.nodes import ( - TypeInfo, ClassDef, Block, ARG_POS, ARG_OPT, ARG_STAR, SymbolTable, - COVARIANT, TypeAlias + TypeInfo, ClassDef, FuncDef, Block, ARG_POS, ARG_OPT, ARG_STAR, SymbolTable, + COVARIANT, TypeAlias, SymbolTableNode, MDEF, ) @@ -62,8 +63,11 @@ def make_type_var(name: str, id: int, values: List[Type], upper_bound: Type, typevars=['T'], variances=[COVARIANT]) # class tuple self.type_typei = self.make_type_info('builtins.type') # class type + self.bool_type_info = self.make_type_info('builtins.bool') + self._add_bool_dunder(self.bool_type_info) self.functioni = self.make_type_info('builtins.function') # function TODO self.ai = self.make_type_info('A', mro=[self.oi]) # class A + self._add_bool_dunder(self.ai) self.bi = self.make_type_info('B', mro=[self.ai, self.oi]) # class B(A) self.ci = self.make_type_info('C', mro=[self.ai, self.oi]) # class C(A) self.di = self.make_type_info('D', mro=[self.oi]) # class D @@ -165,6 +169,12 @@ def make_type_var(name: str, id: int, values: List[Type], upper_bound: Type, self.type_t = TypeType.make_normalized(self.t) self.type_any = TypeType.make_normalized(self.anyt) + def _add_bool_dunder(self, type_info: TypeInfo) -> None: + signature = CallableType([], [], [], Instance(self.bool_type_info, []), None) + bool_func = FuncDef('__bool__', [], Block([])) + bool_func.type = set_callable_name(signature, bool_func) + type_info.names[bool_func.name] = SymbolTableNode(MDEF, bool_func) + # Helper methods def callable(self, *a: Type) -> CallableType: diff --git a/test-data/unit/check-dynamic-typing.test b/test-data/unit/check-dynamic-typing.test index 87bb134cb33c2..573d30bb87f20 100644 --- a/test-data/unit/check-dynamic-typing.test +++ b/test-data/unit/check-dynamic-typing.test @@ -212,8 +212,9 @@ class C: pass [file builtins.py] class object: def __init__(self): pass -class bool: pass -class int: pass +class int: + def __bool__(self) -> bool: pass +class bool(int): pass class type: pass class function: pass class str: pass diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test index 5200c00d3f280..fdf9fe60d6013 100644 --- a/test-data/unit/check-enum.test +++ b/test-data/unit/check-enum.test @@ -894,13 +894,13 @@ else: [case testEnumReachabilityWithNone] # flags: --strict-optional -from enum import Enum +from enum import Flag from typing import Optional -class Foo(Enum): +class Foo(Flag): A = 1 B = 2 - C = 3 + C = 4 x: Optional[Foo] if x: diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 5d3d6b66d7b8a..91c48a474bc6a 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -323,7 +323,8 @@ if int(): b = b or a # E: Incompatible types in assignment (expression has type "Union[bool, A]", variable has type "bool") if int(): b = a or b # E: Incompatible types in assignment (expression has type "Union[A, bool]", variable has type "bool") -class A: pass +class A: + def __bool__(self) -> bool: pass [builtins fixtures/bool.pyi] @@ -375,8 +376,10 @@ if int(): d = c or d # E: Incompatible types in assignment (expression has type "C", variable has type "D") if int(): d = d or c # E: Incompatible types in assignment (expression has type "C", variable has type "D") -class C: pass -class D(C): pass +class C: + def __bool__(self) -> bool: pass +class D(C): + def __bool__(self) -> bool: pass [builtins fixtures/bool.pyi] [case testInOperator] diff --git a/test-data/unit/check-inference-context.test b/test-data/unit/check-inference-context.test index 82a412eeb3593..5a65b9749820d 100644 --- a/test-data/unit/check-inference-context.test +++ b/test-data/unit/check-inference-context.test @@ -749,8 +749,10 @@ if int(): if int(): b = b or c # E: Incompatible types in assignment (expression has type "Union[List[B], List[C]]", variable has type "List[B]") -class A: pass -class B: pass +class A: + def __bool__(self) -> bool: ... +class B: + def __bool__(self) -> bool: ... class C(B): pass [builtins fixtures/list.pyi] diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index e96d72c7358b3..59e7b4d3daefb 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -3294,7 +3294,7 @@ q: Union[Truth, NoAnswerSpecified] if q: reveal_type(q) # N: Revealed type is "Union[__main__.Truth, __main__.NoAnswerSpecified]" else: - reveal_type(q) # N: Revealed type is "__main__.NoAnswerSpecified" + reveal_type(q) # E: Statement is unreachable w: Union[Truth, AlsoTruth] diff --git a/test-data/unit/check-typevar-values.test b/test-data/unit/check-typevar-values.test index 3f77996ec9596..8fb3b07e78a05 100644 --- a/test-data/unit/check-typevar-values.test +++ b/test-data/unit/check-typevar-values.test @@ -86,8 +86,9 @@ class B: def g(self) -> B: return B() AB = TypeVar('AB', A, B) def f(x: AB) -> AB: + indeterminate: bool y = x - if y: + if indeterminate: return y.f() else: return y.g() # E: Incompatible return value type (got "B", expected "A") diff --git a/test-data/unit/fixtures/bool.pyi b/test-data/unit/fixtures/bool.pyi index cef2e9129fb70..46b460c900bd5 100644 --- a/test-data/unit/fixtures/bool.pyi +++ b/test-data/unit/fixtures/bool.pyi @@ -10,9 +10,11 @@ class object: class type: pass class tuple(Generic[T]): pass class function: pass -class int: pass +class int: + def __bool__(self) -> 'bool': pass class bool(int): pass class float: pass -class str: pass +class str: + def __len__(self) -> int: pass class unicode: pass class ellipsis: pass diff --git a/test-data/unit/fixtures/isinstancelist.pyi b/test-data/unit/fixtures/isinstancelist.pyi index fcc3032183aa9..9dd12160fc451 100644 --- a/test-data/unit/fixtures/isinstancelist.pyi +++ b/test-data/unit/fixtures/isinstancelist.pyi @@ -18,11 +18,13 @@ def issubclass(x: object, t: Union[type, Tuple]) -> bool: pass class int: def __add__(self, x: int) -> int: pass + def __bool__(self) -> bool: pass class float: pass class bool(int): pass class str: def __add__(self, x: str) -> str: pass def __getitem__(self, x: int) -> str: pass + def __len__(self) -> int: pass T = TypeVar('T') KT = TypeVar('KT') diff --git a/test-data/unit/fixtures/list.pyi b/test-data/unit/fixtures/list.pyi index c4baf89ffc13a..9c206aa678531 100644 --- a/test-data/unit/fixtures/list.pyi +++ b/test-data/unit/fixtures/list.pyi @@ -16,6 +16,7 @@ class list(Sequence[T]): @overload def __init__(self, x: Iterable[T]) -> None: pass def __iter__(self) -> Iterator[T]: pass + def __len__(self) -> int: pass def __contains__(self, item: object) -> bool: pass def __add__(self, x: list[T]) -> list[T]: pass def __mul__(self, x: int) -> list[T]: pass @@ -26,7 +27,8 @@ class list(Sequence[T]): class tuple(Generic[T]): pass class function: pass -class int: pass +class int: + def __bool__(self) -> bool: pass class float: pass class str: pass class bool(int): pass diff --git a/test-data/unit/fixtures/ops.pyi b/test-data/unit/fixtures/ops.pyi index d5845aba43c63..694263a09b57f 100644 --- a/test-data/unit/fixtures/ops.pyi +++ b/test-data/unit/fixtures/ops.pyi @@ -24,12 +24,13 @@ class tuple(Sequence[Tco]): class function: pass -class bool: pass +class bool(int): pass class str: def __init__(self, x: 'int') -> None: pass def __add__(self, x: 'str') -> 'str': pass def __eq__(self, x: object) -> bool: pass + def __len__(self) -> int: pass def startswith(self, x: 'str') -> bool: pass def strip(self) -> 'str': pass @@ -55,6 +56,7 @@ class int: def __le__(self, x: 'int') -> bool: pass def __gt__(self, x: 'int') -> bool: pass def __ge__(self, x: 'int') -> bool: pass + def __bool__(self) -> bool: pass class float: def __add__(self, x: 'float') -> 'float': pass diff --git a/test-data/unit/fixtures/primitives.pyi b/test-data/unit/fixtures/primitives.pyi index 71f59a9c1d8cf..eec8a554de78a 100644 --- a/test-data/unit/fixtures/primitives.pyi +++ b/test-data/unit/fixtures/primitives.pyi @@ -17,6 +17,7 @@ class int: def __init__(self, x: object = ..., base: int = ...) -> None: pass def __add__(self, i: int) -> int: pass def __rmul__(self, x: int) -> int: pass + def __bool__(self) -> bool: pass class float: def __float__(self) -> float: pass class complex: pass diff --git a/test-data/unit/lib-stub/builtins.pyi b/test-data/unit/lib-stub/builtins.pyi index 57fc4b8f0de29..96cd6dd66427d 100644 --- a/test-data/unit/lib-stub/builtins.pyi +++ b/test-data/unit/lib-stub/builtins.pyi @@ -11,11 +11,14 @@ class type: # These are provided here for convenience. class int: def __add__(self, other: int) -> int: pass + def __bool__(self) -> bool: pass class bool(int): pass class float: pass -class str: pass -class bytes: pass +class str: + def __len__(self) -> int: pass +class bytes: + def __len__(self) -> int: pass class function: pass class ellipsis: pass diff --git a/test-data/unit/lib-stub/enum.pyi b/test-data/unit/lib-stub/enum.pyi index 2c1b03532a5bc..2331071c4cd24 100644 --- a/test-data/unit/lib-stub/enum.pyi +++ b/test-data/unit/lib-stub/enum.pyi @@ -35,6 +35,7 @@ def unique(enumeration: _T) -> _T: pass class Flag(Enum): def __or__(self: _T, other: Union[int, _T]) -> _T: pass + def __bool__(self) -> bool: ... class IntFlag(int, Flag): diff --git a/test-data/unit/lib-stub/typing.pyi b/test-data/unit/lib-stub/typing.pyi index 2f42633843e0e..6f0d86a96cf01 100644 --- a/test-data/unit/lib-stub/typing.pyi +++ b/test-data/unit/lib-stub/typing.pyi @@ -42,6 +42,7 @@ class Generator(Iterator[T], Generic[T, U, V]): class Sequence(Iterable[T_co]): def __getitem__(self, n: Any) -> T_co: pass + def __len__(self) -> int: pass class Mapping(Generic[T, T_co]): pass From 76bd3e29446db8871e2d68d043aa393ae90b631b Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Thu, 24 Jun 2021 23:32:05 -0400 Subject: [PATCH 03/31] Undo unneeded test change --- test-data/unit/check-expressions.test | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index 91c48a474bc6a..d8eec99d78211 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -378,8 +378,7 @@ if int(): d = d or c # E: Incompatible types in assignment (expression has type "C", variable has type "D") class C: def __bool__(self) -> bool: pass -class D(C): - def __bool__(self) -> bool: pass +class D(C): pass [builtins fixtures/bool.pyi] [case testInOperator] From 801f274b837a254557d2e39890d1de59b190744b Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Thu, 24 Jun 2021 23:33:04 -0400 Subject: [PATCH 04/31] =?UTF-8?q?Let=E2=80=99s=20try=20to=20just=20warn=20?= =?UTF-8?q?in=20boolean=20context=20(no=20tests=20yet)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mypy/checker.py | 72 ++++++++++++++++++++++++++++++++----------------- mypy/types.py | 8 ------ 2 files changed, 48 insertions(+), 32 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 429ea155dbcd9..8b2431fe5f73b 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3971,6 +3971,25 @@ def conditional_callable_type_map(self, expr: Expression, return None, {} + @staticmethod + def _is_truthy_instance(t: Type) -> bool: + return ( + isinstance(t, Instance) and + t.type and + not t.type.has_readable_member('__bool__') and + not t.type.has_readable_member('__len__') + ) + + def _check_for_truthy_type(self, t: Type, node: Node) -> None: + if self._is_truthy_instance(t): + self.msg.note( + "{} has type '{}' which does not implement __bool__ or __len__ " + "so it will always be truthy in boolean context".format(node, t), node) + elif isinstance(t, UnionType) and all(self._is_truthy_instance(t) for t in t.items): + self.msg.note( + "{} has type '{}' where none of the members implement __bool__ or __len__ " + "so it will always be truthy in boolean context".format(node, t), node) + def find_type_equals_check(self, node: ComparisonExpr, expr_indices: List[int] ) -> Tuple[TypeMap, TypeMap]: """Narrow types based on any checks of the type ``type(x) == T`` @@ -4073,30 +4092,30 @@ def find_isinstance_check_helper(self, node: Expression) -> Tuple[TypeMap, TypeM elif is_false_literal(node): return None, {} elif isinstance(node, CallExpr): - if len(node.args) == 0: - return {}, {} - expr = collapse_walrus(node.args[0]) - if refers_to_fullname(node.callee, 'builtins.isinstance'): - if len(node.args) != 2: # the error will be reported elsewhere - return {}, {} - if literal(expr) == LITERAL_TYPE: - return self.conditional_type_map_with_intersection( - expr, - type_map[expr], - get_isinstance_type(node.args[1], type_map), - ) - elif refers_to_fullname(node.callee, 'builtins.issubclass'): - if len(node.args) != 2: # the error will be reported elsewhere - return {}, {} - if literal(expr) == LITERAL_TYPE: - return self.infer_issubclass_maps(node, expr, type_map) - elif refers_to_fullname(node.callee, 'builtins.callable'): - if len(node.args) != 1: # the error will be reported elsewhere - return {}, {} - if literal(expr) == LITERAL_TYPE: - vartype = type_map[expr] - return self.conditional_callable_type_map(expr, vartype) - elif isinstance(node.callee, RefExpr): + if len(node.args) > 0: + expr = collapse_walrus(node.args[0]) + if refers_to_fullname(node.callee, 'builtins.isinstance'): + if len(node.args) != 2: # the error will be reported elsewhere + return {}, {} + if literal(expr) == LITERAL_TYPE: + return self.conditional_type_map_with_intersection( + expr, + type_map[expr], + get_isinstance_type(node.args[1], type_map), + ) + elif refers_to_fullname(node.callee, 'builtins.issubclass'): + if len(node.args) != 2: # the error will be reported elsewhere + return {}, {} + if literal(expr) == LITERAL_TYPE: + return self.infer_issubclass_maps(node, expr, type_map) + elif refers_to_fullname(node.callee, 'builtins.callable'): + if len(node.args) != 1: # the error will be reported elsewhere + return {}, {} + if literal(expr) == LITERAL_TYPE: + vartype = type_map[expr] + return self.conditional_callable_type_map(expr, vartype) + + if isinstance(node.callee, RefExpr): if node.callee.type_guard is not None: # TODO: Follow keyword args or *args, **kwargs if node.arg_kinds[0] != nodes.ARG_POS: @@ -4104,6 +4123,10 @@ def find_isinstance_check_helper(self, node: Expression) -> Tuple[TypeMap, TypeM return {}, {} if literal(expr) == LITERAL_TYPE: return {expr: TypeGuardType(node.callee.type_guard)}, {} + + self._check_for_truthy_type(type_map[node], node) + + return {}, {} elif isinstance(node, ComparisonExpr): # Step 1: Obtain the types of each operand and whether or not we can # narrow their types. (For example, we shouldn't try narrowing the @@ -4255,6 +4278,7 @@ def has_no_custom_eq_checks(t: Type) -> bool: # Restrict the type of the variable to True-ish/False-ish in the if and else branches # respectively vartype = type_map[node] + self._check_for_truthy_type(vartype, node) if_type = true_only(vartype) # type: Type else_type = false_only(vartype) # type: Type ref = node # type: Expression diff --git a/mypy/types.py b/mypy/types.py index b8264f239ec6a..7aa83a75f431b 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -842,14 +842,6 @@ def __init__(self, typ: mypy.nodes.TypeInfo, args: Sequence[Type], # Literal context. self.last_known_value = last_known_value - if ( - self.type and - not self.type.has_readable_member('__bool__') and - not self.type.has_readable_member('__len__') - ): - self.can_be_true = True - self.can_be_false = False - def accept(self, visitor: 'TypeVisitor[T]') -> T: return visitor.visit_instance(self) From cbcd9d92650ad5b1f2bc9fb858c09ce542b84e08 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Fri, 25 Jun 2021 02:58:55 -0400 Subject: [PATCH 05/31] disable for non-strict optional + message --- mypy/checker.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 8b2431fe5f73b..a35bc5f9a96f8 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3980,15 +3980,28 @@ def _is_truthy_instance(t: Type) -> bool: not t.type.has_readable_member('__len__') ) - def _check_for_truthy_type(self, t: Type, node: Node) -> None: + def _format_expr_name(self, node: Node, t: Type): + if isinstance(node, RefExpr): + return f'"{node.fullname}" has type "{t}"' + elif isinstance(node, CallExpr): + if isinstance(node.callee, RefExpr): + return f'"{node.callee.fullname}" returns "{t}"' + else: + return f'call returns "{t}"' + else: + return f'expression has type "{t}"' + + def _check_for_truthy_type(self, t: Type, expr: Expression) -> None: + if not state.strict_optional: + return # if everything can be None, all bets are off if self._is_truthy_instance(t): self.msg.note( - "{} has type '{}' which does not implement __bool__ or __len__ " - "so it will always be truthy in boolean context".format(node, t), node) + '{} which does not implement __bool__ or __len__ ' + 'so it will always be truthy in boolean context'.format(self._format_expr_name(expr, t)), expr) elif isinstance(t, UnionType) and all(self._is_truthy_instance(t) for t in t.items): self.msg.note( - "{} has type '{}' where none of the members implement __bool__ or __len__ " - "so it will always be truthy in boolean context".format(node, t), node) + "{} none of which implement __bool__ or __len__ " + "so it will always be truthy in boolean context".format(self._format_expr_name(expr, t)), expr) def find_type_equals_check(self, node: ComparisonExpr, expr_indices: List[int] ) -> Tuple[TypeMap, TypeMap]: From c58bb6a810167998bcfcfe3125a2c7d7e40e4b25 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Fri, 25 Jun 2021 10:56:19 -0400 Subject: [PATCH 06/31] futile attempt to improve _format_expr_name --- mypy/checker.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index a35bc5f9a96f8..9ea6e2e63bcbc 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3984,10 +3984,11 @@ def _format_expr_name(self, node: Node, t: Type): if isinstance(node, RefExpr): return f'"{node.fullname}" has type "{t}"' elif isinstance(node, CallExpr): - if isinstance(node.callee, RefExpr): + if isinstance(node.callee, MemberExpr): + return f'"{node.callee.name}" returns "{t}"' + elif isinstance(node.callee, RefExpr) and node.callee.fullname: return f'"{node.callee.fullname}" returns "{t}"' - else: - return f'call returns "{t}"' + return f'call returns "{t}"' else: return f'expression has type "{t}"' From 6e497bb858d302e293564ad70ab526e45f4a4725 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Fri, 25 Jun 2021 12:32:56 -0400 Subject: [PATCH 07/31] kick CI From f0a5081bafd0a77a1c9b91d1aca894c8b9438a40 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Fri, 25 Jun 2021 13:05:31 -0400 Subject: [PATCH 08/31] even more futile attempts on _format_expr_name --- mypy/checker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mypy/checker.py b/mypy/checker.py index 9ea6e2e63bcbc..2ab4798b86036 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3981,7 +3981,9 @@ def _is_truthy_instance(t: Type) -> bool: ) def _format_expr_name(self, node: Node, t: Type): - if isinstance(node, RefExpr): + if isinstance(node, MemberExpr): + return f'member "{node.name}" has type "{t}"' + elif isinstance(node, RefExpr) and node.fullname: return f'"{node.fullname}" has type "{t}"' elif isinstance(node, CallExpr): if isinstance(node.callee, MemberExpr): From 12278df851c2eb7b77a8319ed8ba3b2b3d17d2c7 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Fri, 25 Jun 2021 13:20:22 -0400 Subject: [PATCH 09/31] add error code --- mypy/checker.py | 8 ++++++-- mypy/errorcodes.py | 6 ++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 2ab4798b86036..d55ca6fe9e0e1 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4000,11 +4000,15 @@ def _check_for_truthy_type(self, t: Type, expr: Expression) -> None: if self._is_truthy_instance(t): self.msg.note( '{} which does not implement __bool__ or __len__ ' - 'so it will always be truthy in boolean context'.format(self._format_expr_name(expr, t)), expr) + 'so it will always be truthy in boolean context'.format(self._format_expr_name(expr, t)), expr, + code=codes.IMPLICIT_BOOL, + ) elif isinstance(t, UnionType) and all(self._is_truthy_instance(t) for t in t.items): self.msg.note( "{} none of which implement __bool__ or __len__ " - "so it will always be truthy in boolean context".format(self._format_expr_name(expr, t)), expr) + "so it will always be truthy in boolean context".format(self._format_expr_name(expr, t)), expr, + code=codes.IMPLICIT_BOOL, + ) def find_type_equals_check(self, node: ComparisonExpr, expr_indices: List[int] ) -> Tuple[TypeMap, TypeMap]: diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 01b946c467477..89175676c387f 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -117,6 +117,12 @@ def __str__(self) -> str: "Warn about redundant expressions", 'General', default_enabled=False) # type: Final +IMPLICIT_BOOL = ErrorCode( + 'implicit-bool', + "Warn when an expression whose type does not implement __bool__ or __len__ is used in boolean context " + "since it would always evaluate as true", + 'General', + default_enabled=False) # type: Final NAME_MATCH = ErrorCode( 'name-match', "Check that type definition has consistent naming", 'General') # type: Final From bcf870681069eee651529b68d539fa2e0fbdd924 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Fri, 25 Jun 2021 13:33:16 -0400 Subject: [PATCH 10/31] docs --- docs/source/error_code_list2.rst | 15 +++++++++++++++ mypy/checker.py | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index d88525f33bb21..c0f60472e5167 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -214,3 +214,18 @@ mypy generates an error if it thinks that an expression is redundant. # Error: If condition in comprehension is always true [redundant-expr] [i for i in range(x) if isinstance(i, int)] + + +Check that expression is not implicitly true in boolean context [implicit-bool] +-------------------------------------------------------------------------------- + +Warn when an expression whose type does not implement __bool__ or __len__ is used in boolean context +since it would always evaluate as true. + +.. code-block:: python + class Foo: + pass + foo = Foo() + # Error: "foo" has type "Foo" which does not implement __bool__ or __len__ so it will always be true in boolean context + if foo: + ... diff --git a/mypy/checker.py b/mypy/checker.py index d55ca6fe9e0e1..2707cccc59a8e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4000,13 +4000,13 @@ def _check_for_truthy_type(self, t: Type, expr: Expression) -> None: if self._is_truthy_instance(t): self.msg.note( '{} which does not implement __bool__ or __len__ ' - 'so it will always be truthy in boolean context'.format(self._format_expr_name(expr, t)), expr, + 'so it will always be true in boolean context'.format(self._format_expr_name(expr, t)), expr, code=codes.IMPLICIT_BOOL, ) elif isinstance(t, UnionType) and all(self._is_truthy_instance(t) for t in t.items): self.msg.note( "{} none of which implement __bool__ or __len__ " - "so it will always be truthy in boolean context".format(self._format_expr_name(expr, t)), expr, + "so it will always be true in boolean context".format(self._format_expr_name(expr, t)), expr, code=codes.IMPLICIT_BOOL, ) From 050bd67b41459bc7bdbd08088721275385544a19 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Fri, 25 Jun 2021 14:06:35 -0400 Subject: [PATCH 11/31] fix type_guard support --- mypy/checker.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 2707cccc59a8e..faafe1694c5f7 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4134,9 +4134,7 @@ def find_isinstance_check_helper(self, node: Expression) -> Tuple[TypeMap, TypeM if literal(expr) == LITERAL_TYPE: vartype = type_map[expr] return self.conditional_callable_type_map(expr, vartype) - - if isinstance(node.callee, RefExpr): - if node.callee.type_guard is not None: + elif isinstance(node.callee, RefExpr) and node.callee.type_guard is not None: # TODO: Follow keyword args or *args, **kwargs if node.arg_kinds[0] != nodes.ARG_POS: self.fail("Type guard requires positional argument", node) From d2851a3a3ae0af80796d2a92abb7f4f53ba84213 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Fri, 25 Jun 2021 14:22:40 -0400 Subject: [PATCH 12/31] fix _format_expr_name sig --- mypy/checker.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index faafe1694c5f7..19892597de943 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3980,16 +3980,16 @@ def _is_truthy_instance(t: Type) -> bool: not t.type.has_readable_member('__len__') ) - def _format_expr_name(self, node: Node, t: Type): - if isinstance(node, MemberExpr): - return f'member "{node.name}" has type "{t}"' - elif isinstance(node, RefExpr) and node.fullname: - return f'"{node.fullname}" has type "{t}"' - elif isinstance(node, CallExpr): - if isinstance(node.callee, MemberExpr): - return f'"{node.callee.name}" returns "{t}"' - elif isinstance(node.callee, RefExpr) and node.callee.fullname: - return f'"{node.callee.fullname}" returns "{t}"' + def _format_expr_name(self, expr: Expression, t: Type): + if isinstance(expr, MemberExpr): + return f'member "{expr.name}" has type "{t}"' + elif isinstance(expr, RefExpr) and expr.fullname: + return f'"{expr.fullname}" has type "{t}"' + elif isinstance(expr, CallExpr): + if isinstance(expr.callee, MemberExpr): + return f'"{expr.callee.name}" returns "{t}"' + elif isinstance(expr.callee, RefExpr) and expr.callee.fullname: + return f'"{expr.callee.fullname}" returns "{t}"' return f'call returns "{t}"' else: return f'expression has type "{t}"' From 6f0951d97aa9a268c9e7be343f356f1bce8cd9b8 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Fri, 25 Jun 2021 15:15:51 -0400 Subject: [PATCH 13/31] revert change to testConditionalBoolLiteralUnionNarrowing --- test-data/unit/check-literal.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index 59e7b4d3daefb..e96d72c7358b3 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -3294,7 +3294,7 @@ q: Union[Truth, NoAnswerSpecified] if q: reveal_type(q) # N: Revealed type is "Union[__main__.Truth, __main__.NoAnswerSpecified]" else: - reveal_type(q) # E: Statement is unreachable + reveal_type(q) # N: Revealed type is "__main__.NoAnswerSpecified" w: Union[Truth, AlsoTruth] From dc70b7cac6385ccf210fd2518bf5c868569e3578 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Fri, 25 Jun 2021 15:55:01 -0400 Subject: [PATCH 14/31] handle FunctionLikes + re-add test --- mypy/checker.py | 11 ++++-- test-data/unit/check-errorcodes.test | 58 ++++++++++++++++++++++++++++ test-data/unit/fixtures/list.pyi | 2 +- 3 files changed, 67 insertions(+), 4 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 19892597de943..091a310f53079 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3998,13 +3998,18 @@ def _check_for_truthy_type(self, t: Type, expr: Expression) -> None: if not state.strict_optional: return # if everything can be None, all bets are off if self._is_truthy_instance(t): - self.msg.note( + self.msg.fail( '{} which does not implement __bool__ or __len__ ' 'so it will always be true in boolean context'.format(self._format_expr_name(expr, t)), expr, code=codes.IMPLICIT_BOOL, ) - elif isinstance(t, UnionType) and all(self._is_truthy_instance(t) for t in t.items): - self.msg.note( + elif isinstance(t, FunctionLike): + self.msg.fail( + f'function "{t}" will always be true in boolean context', expr, + code=codes.IMPLICIT_BOOL, + ) + elif isinstance(t, UnionType) and all(self._is_truthy_instance(t) or isinstance(t, FunctionLike) for t in t.items): + self.msg.fail( "{} none of which implement __bool__ or __len__ " "so it will always be true in boolean context".format(self._format_expr_name(expr, t)), expr, code=codes.IMPLICIT_BOOL, diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 7479b15b7bc4d..dc5600b20c73c 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -807,3 +807,61 @@ from typing_extensions import TypedDict Foo = TypedDict("Bar", {}) # E: First argument "Bar" to TypedDict() does not match variable name "Foo" [name-match] [builtins fixtures/dict.pyi] +[case testImplicitBool] +# flags: --enable-error-code implicit-bool +from typing import List, Union + +class Foo: + pass + + +foo = Foo() +if foo: # E: "__main__.foo" has type "__main__.Foo" which does not implement __bool__ or __len__ so it will always be true in boolean context [implicit-bool] + pass + +zero = 0 +if zero: + pass + + +false = False +if false: + pass + + +null = None +if null: + pass + + +s = '' +if s: + pass + + +good_union: Union[str, int] = 5 +if good_union: + pass + + +bad_union: Union[Foo, object] = Foo() +if bad_union: # E: "__main__.bad_union" has type "Union[__main__.Foo, builtins.object]" none of which implement __bool__ or __len__ so it will always be true in boolean context [implicit-bool] + pass + + +def f(): + pass + + +if f: # E: function "def () -> Any" will always be true in boolean context [implicit-bool] + pass + + +lst: List[int] = [] +if lst: + pass + + +conditional_result = 'foo' if f else 'bar' # E: function "def () -> Any" will always be true in boolean context [implicit-bool] + +[builtins fixtures/list.pyi] diff --git a/test-data/unit/fixtures/list.pyi b/test-data/unit/fixtures/list.pyi index 9c206aa678531..7489cd22a9cf7 100644 --- a/test-data/unit/fixtures/list.pyi +++ b/test-data/unit/fixtures/list.pyi @@ -30,7 +30,7 @@ class function: pass class int: def __bool__(self) -> bool: pass class float: pass -class str: pass +class str(Sequence[str]): pass class bool(int): pass property = object() # Dummy definition. From c68df2f9a2873f47c72a5b4b019d024ea3e2513c Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Fri, 25 Jun 2021 16:40:38 -0400 Subject: [PATCH 15/31] fix list fixture --- test-data/unit/fixtures/list.pyi | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test-data/unit/fixtures/list.pyi b/test-data/unit/fixtures/list.pyi index 7489cd22a9cf7..31dc333b3d4f6 100644 --- a/test-data/unit/fixtures/list.pyi +++ b/test-data/unit/fixtures/list.pyi @@ -29,8 +29,10 @@ class tuple(Generic[T]): pass class function: pass class int: def __bool__(self) -> bool: pass -class float: pass -class str(Sequence[str]): pass +class float: + def __bool__(self) -> bool: pass +class str: + def __len__(self) -> bool: pass class bool(int): pass property = object() # Dummy definition. From f346448c9a2749af1de636d1acefe7a4419f265b Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Fri, 25 Jun 2021 23:57:41 -0400 Subject: [PATCH 16/31] fix typing --- mypy/checker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 091a310f53079..7786424897a0e 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3975,12 +3975,12 @@ def conditional_callable_type_map(self, expr: Expression, def _is_truthy_instance(t: Type) -> bool: return ( isinstance(t, Instance) and - t.type and + bool(t.type) and not t.type.has_readable_member('__bool__') and not t.type.has_readable_member('__len__') ) - def _format_expr_name(self, expr: Expression, t: Type): + def _format_expr_name(self, expr: Expression, t: Type) -> str: if isinstance(expr, MemberExpr): return f'member "{expr.name}" has type "{t}"' elif isinstance(expr, RefExpr) and expr.fullname: From d36a54c35435ad5e99fe1b15e45a7cbafd299a7e Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Tue, 29 Jun 2021 22:59:59 -0400 Subject: [PATCH 17/31] fix type errors --- mypy/checker.py | 5 +++-- mypy/test/typefixture.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index fd2b6c8892080..83f391bcfbb73 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3988,7 +3988,7 @@ def conditional_callable_type_map(self, expr: Expression, return None, {} @staticmethod - def _is_truthy_instance(t: Type) -> bool: + def _is_truthy_instance(t: ProperType) -> bool: return ( isinstance(t, Instance) and bool(t.type) and @@ -4011,6 +4011,7 @@ def _format_expr_name(self, expr: Expression, t: Type) -> str: return f'expression has type "{t}"' def _check_for_truthy_type(self, t: Type, expr: Expression) -> None: + t = get_proper_type(t) if not state.strict_optional: return # if everything can be None, all bets are off if self._is_truthy_instance(t): @@ -4024,7 +4025,7 @@ def _check_for_truthy_type(self, t: Type, expr: Expression) -> None: f'function "{t}" will always be true in boolean context', expr, code=codes.IMPLICIT_BOOL, ) - elif isinstance(t, UnionType) and all(self._is_truthy_instance(t) or isinstance(t, FunctionLike) for t in t.items): + elif isinstance(t, UnionType) and all(self._is_truthy_instance(t) or isinstance(t, FunctionLike) for t in get_proper_types(t.items)): self.msg.fail( "{} none of which implement __bool__ or __len__ " "so it will always be true in boolean context".format(self._format_expr_name(expr, t)), expr, diff --git a/mypy/test/typefixture.py b/mypy/test/typefixture.py index c88a0f0e19760..a1e8f72e261c4 100644 --- a/mypy/test/typefixture.py +++ b/mypy/test/typefixture.py @@ -170,7 +170,7 @@ def make_type_var(name: str, id: int, values: List[Type], upper_bound: Type, self.type_any = TypeType.make_normalized(self.anyt) def _add_bool_dunder(self, type_info: TypeInfo) -> None: - signature = CallableType([], [], [], Instance(self.bool_type_info, []), None) + signature = CallableType([], [], [], Instance(self.bool_type_info, []), self.function) bool_func = FuncDef('__bool__', [], Block([])) bool_func.type = set_callable_name(signature, bool_func) type_info.names[bool_func.name] = SymbolTableNode(MDEF, bool_func) From b9dd5484932fb62b66bbfad7e9916275bcafce31 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Tue, 29 Jun 2021 23:38:19 -0400 Subject: [PATCH 18/31] fix TypeFixture --- mypy/test/typefixture.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy/test/typefixture.py b/mypy/test/typefixture.py index a1e8f72e261c4..c7d891736a445 100644 --- a/mypy/test/typefixture.py +++ b/mypy/test/typefixture.py @@ -64,10 +64,8 @@ def make_type_var(name: str, id: int, values: List[Type], upper_bound: Type, variances=[COVARIANT]) # class tuple self.type_typei = self.make_type_info('builtins.type') # class type self.bool_type_info = self.make_type_info('builtins.bool') - self._add_bool_dunder(self.bool_type_info) self.functioni = self.make_type_info('builtins.function') # function TODO self.ai = self.make_type_info('A', mro=[self.oi]) # class A - self._add_bool_dunder(self.ai) self.bi = self.make_type_info('B', mro=[self.ai, self.oi]) # class B(A) self.ci = self.make_type_info('C', mro=[self.ai, self.oi]) # class C(A) self.di = self.make_type_info('D', mro=[self.oi]) # class D @@ -169,6 +167,9 @@ def make_type_var(name: str, id: int, values: List[Type], upper_bound: Type, self.type_t = TypeType.make_normalized(self.t) self.type_any = TypeType.make_normalized(self.anyt) + self._add_bool_dunder(self.bool_type_info) + self._add_bool_dunder(self.ai) + def _add_bool_dunder(self, type_info: TypeInfo) -> None: signature = CallableType([], [], [], Instance(self.bool_type_info, []), self.function) bool_func = FuncDef('__bool__', [], Block([])) From 212af0945d787ad1bd10e9bae70796d10526fbf7 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Wed, 30 Jun 2021 09:36:11 -0400 Subject: [PATCH 19/31] fix sphinx syntax --- docs/source/error_code_list2.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index c0f60472e5167..7cf2997f14740 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -223,6 +223,7 @@ Warn when an expression whose type does not implement __bool__ or __len__ is use since it would always evaluate as true. .. code-block:: python + class Foo: pass foo = Foo() From 246d06568d1595424b109d30487898e24d2f549a Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Wed, 30 Jun 2021 09:36:20 -0400 Subject: [PATCH 20/31] restructure _check_for_truthy_type --- mypy/checker.py | 65 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 83f391bcfbb73..1b6964a9ea201 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3996,39 +3996,60 @@ def _is_truthy_instance(t: ProperType) -> bool: not t.type.has_readable_member('__len__') ) - def _format_expr_name(self, expr: Expression, t: Type) -> str: - if isinstance(expr, MemberExpr): - return f'member "{expr.name}" has type "{t}"' - elif isinstance(expr, RefExpr) and expr.fullname: - return f'"{expr.fullname}" has type "{t}"' - elif isinstance(expr, CallExpr): - if isinstance(expr.callee, MemberExpr): - return f'"{expr.callee.name}" returns "{t}"' - elif isinstance(expr.callee, RefExpr) and expr.callee.fullname: - return f'"{expr.callee.fullname}" returns "{t}"' - return f'call returns "{t}"' - else: - return f'expression has type "{t}"' + def _is_truthy_type(self, t: ProperType) -> bool: + return ( + ( + isinstance(t, Instance) and + bool(t.type) and + not t.type.has_readable_member('__bool__') and + not t.type.has_readable_member('__len__') + ) + or isinstance(t, FunctionLike) + or ( + isinstance(t, UnionType) and + all(self._is_truthy_type(t) for t in get_proper_types(t.items)) + ) + ) def _check_for_truthy_type(self, t: Type, expr: Expression) -> None: - t = get_proper_type(t) if not state.strict_optional: return # if everything can be None, all bets are off - if self._is_truthy_instance(t): + + t = get_proper_type(t) + if not self._is_truthy_type(t): + return + + def format_expr_type() -> str: + if isinstance(expr, MemberExpr): + return f'member "{expr.name}" has type "{t}"' + elif isinstance(expr, RefExpr) and expr.fullname: + return f'"{expr.fullname}" has type "{t}"' + elif isinstance(expr, CallExpr): + if isinstance(expr.callee, MemberExpr): + return f'"{expr.callee.name}" returns "{t}"' + elif isinstance(expr.callee, RefExpr) and expr.callee.fullname: + return f'"{expr.callee.fullname}" returns "{t}"' + return f'call returns "{t}"' + else: + return f'expression has type "{t}"' + + if isinstance(t, FunctionLike): self.msg.fail( - '{} which does not implement __bool__ or __len__ ' - 'so it will always be true in boolean context'.format(self._format_expr_name(expr, t)), expr, + f'function "{t}" will always be true in boolean context', expr, code=codes.IMPLICIT_BOOL, ) - elif isinstance(t, FunctionLike): + elif isinstance(t, UnionType): self.msg.fail( - f'function "{t}" will always be true in boolean context', expr, + f"{format_expr_type()} none of which implement __bool__ or __len__ " + "so it will always be true in boolean context", + expr, code=codes.IMPLICIT_BOOL, ) - elif isinstance(t, UnionType) and all(self._is_truthy_instance(t) or isinstance(t, FunctionLike) for t in get_proper_types(t.items)): + else: self.msg.fail( - "{} none of which implement __bool__ or __len__ " - "so it will always be true in boolean context".format(self._format_expr_name(expr, t)), expr, + f'{format_expr_type()} which does not implement __bool__ or __len__ ' + 'so it will always be true in boolean context', + expr, code=codes.IMPLICIT_BOOL, ) From 9e19e85d0d4f027a310c1ad4b5a2959fa47545c9 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Wed, 30 Jun 2021 11:20:04 -0400 Subject: [PATCH 21/31] Rewrite implicit_bool short description --- mypy/errorcodes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index 726b68d9df672..a25b8b9bd68a2 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -124,8 +124,7 @@ def __str__(self) -> str: ) IMPLICIT_BOOL: Final = ErrorCode( 'implicit-bool', - "Warn when an expression whose type does not implement __bool__ or __len__ is used in boolean context " - "since it would always evaluate as true", + "Warn about potential use of implicit bool (that always returns true)", 'General', default_enabled=False ) From 1753f2cf1c385eda86c53ce2e9dfbc7c1f260283 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Wed, 30 Jun 2021 23:35:34 -0400 Subject: [PATCH 22/31] Remove unused _is_truthy_instance --- mypy/checker.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 1b6964a9ea201..4539e63f3fc39 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -3987,15 +3987,6 @@ def conditional_callable_type_map(self, expr: Expression, return None, {} - @staticmethod - def _is_truthy_instance(t: ProperType) -> bool: - return ( - isinstance(t, Instance) and - bool(t.type) and - not t.type.has_readable_member('__bool__') and - not t.type.has_readable_member('__len__') - ) - def _is_truthy_type(self, t: ProperType) -> bool: return ( ( From e60082df41522fa88882ad2868a7d2354d68ac10 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Wed, 30 Jun 2021 23:47:47 -0400 Subject: [PATCH 23/31] why did I ever change this code? --- mypy/checker.py | 53 ++++++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index 4539e63f3fc39..f9c18a365ca95 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4146,39 +4146,38 @@ def find_isinstance_check_helper(self, node: Expression) -> Tuple[TypeMap, TypeM elif is_false_literal(node): return None, {} elif isinstance(node, CallExpr): - if len(node.args) > 0: - expr = collapse_walrus(node.args[0]) - if refers_to_fullname(node.callee, 'builtins.isinstance'): - if len(node.args) != 2: # the error will be reported elsewhere - return {}, {} - if literal(expr) == LITERAL_TYPE: - return self.conditional_type_map_with_intersection( - expr, - type_map[expr], - get_isinstance_type(node.args[1], type_map), - ) - elif refers_to_fullname(node.callee, 'builtins.issubclass'): - if len(node.args) != 2: # the error will be reported elsewhere - return {}, {} - if literal(expr) == LITERAL_TYPE: - return self.infer_issubclass_maps(node, expr, type_map) - elif refers_to_fullname(node.callee, 'builtins.callable'): - if len(node.args) != 1: # the error will be reported elsewhere - return {}, {} - if literal(expr) == LITERAL_TYPE: - vartype = type_map[expr] - return self.conditional_callable_type_map(expr, vartype) - elif isinstance(node.callee, RefExpr) and node.callee.type_guard is not None: + self._check_for_truthy_type(type_map[node], node) + if len(node.args) == 0: + return {}, {} + expr = collapse_walrus(node.args[0]) + if refers_to_fullname(node.callee, 'builtins.isinstance'): + if len(node.args) != 2: # the error will be reported elsewhere + return {}, {} + if literal(expr) == LITERAL_TYPE: + return self.conditional_type_map_with_intersection( + expr, + type_map[expr], + get_isinstance_type(node.args[1], type_map), + ) + elif refers_to_fullname(node.callee, 'builtins.issubclass'): + if len(node.args) != 2: # the error will be reported elsewhere + return {}, {} + if literal(expr) == LITERAL_TYPE: + return self.infer_issubclass_maps(node, expr, type_map) + elif refers_to_fullname(node.callee, 'builtins.callable'): + if len(node.args) != 1: # the error will be reported elsewhere + return {}, {} + if literal(expr) == LITERAL_TYPE: + vartype = type_map[expr] + return self.conditional_callable_type_map(expr, vartype) + elif isinstance(node.callee, RefExpr): + if node.callee.type_guard is not None: # TODO: Follow keyword args or *args, **kwargs if node.arg_kinds[0] != nodes.ARG_POS: self.fail("Type guard requires positional argument", node) return {}, {} if literal(expr) == LITERAL_TYPE: return {expr: TypeGuardType(node.callee.type_guard)}, {} - - self._check_for_truthy_type(type_map[node], node) - - return {}, {} elif isinstance(node, ComparisonExpr): # Step 1: Obtain the types of each operand and whether or not we can # narrow their types. (For example, we shouldn't try narrowing the From 250c19bcc9328e08a5d2a6e5b5cfc50dda8686f1 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Wed, 4 Aug 2021 21:32:18 -0400 Subject: [PATCH 24/31] testImplicitBool: clean up newlines --- test-data/unit/check-errorcodes.test | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index dc5600b20c73c..8cdd5f97c3ae0 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -814,7 +814,6 @@ from typing import List, Union class Foo: pass - foo = Foo() if foo: # E: "__main__.foo" has type "__main__.Foo" which does not implement __bool__ or __len__ so it will always be true in boolean context [implicit-bool] pass @@ -823,45 +822,33 @@ zero = 0 if zero: pass - false = False if false: pass - null = None if null: pass - s = '' if s: pass - good_union: Union[str, int] = 5 if good_union: pass - bad_union: Union[Foo, object] = Foo() if bad_union: # E: "__main__.bad_union" has type "Union[__main__.Foo, builtins.object]" none of which implement __bool__ or __len__ so it will always be true in boolean context [implicit-bool] pass - def f(): pass - - if f: # E: function "def () -> Any" will always be true in boolean context [implicit-bool] pass - +conditional_result = 'foo' if f else 'bar' # E: function "def () -> Any" will always be true in boolean context [implicit-bool] lst: List[int] = [] if lst: pass - - -conditional_result = 'foo' if f else 'bar' # E: function "def () -> Any" will always be true in boolean context [implicit-bool] - [builtins fixtures/list.pyi] From 7166b54a7ac6e8c73a7c0bd1869d15c577e913d4 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Wed, 4 Aug 2021 21:34:38 -0400 Subject: [PATCH 25/31] Revert most of the changes to test fixtures --- test-data/unit/check-dynamic-typing.test | 5 ++--- test-data/unit/check-enum.test | 6 +++--- test-data/unit/check-expressions.test | 6 ++---- test-data/unit/check-inference-context.test | 6 ++---- test-data/unit/check-typevar-values.test | 3 +-- test-data/unit/fixtures/bool.pyi | 6 ++---- test-data/unit/fixtures/dict.pyi | 1 - test-data/unit/fixtures/isinstancelist.pyi | 2 -- test-data/unit/fixtures/ops.pyi | 4 +--- test-data/unit/fixtures/primitives.pyi | 1 - test-data/unit/lib-stub/builtins.pyi | 7 ++----- test-data/unit/lib-stub/enum.pyi | 1 - test-data/unit/lib-stub/typing.pyi | 1 - 13 files changed, 15 insertions(+), 34 deletions(-) diff --git a/test-data/unit/check-dynamic-typing.test b/test-data/unit/check-dynamic-typing.test index 573d30bb87f20..87bb134cb33c2 100644 --- a/test-data/unit/check-dynamic-typing.test +++ b/test-data/unit/check-dynamic-typing.test @@ -212,9 +212,8 @@ class C: pass [file builtins.py] class object: def __init__(self): pass -class int: - def __bool__(self) -> bool: pass -class bool(int): pass +class bool: pass +class int: pass class type: pass class function: pass class str: pass diff --git a/test-data/unit/check-enum.test b/test-data/unit/check-enum.test index fdf9fe60d6013..5200c00d3f280 100644 --- a/test-data/unit/check-enum.test +++ b/test-data/unit/check-enum.test @@ -894,13 +894,13 @@ else: [case testEnumReachabilityWithNone] # flags: --strict-optional -from enum import Flag +from enum import Enum from typing import Optional -class Foo(Flag): +class Foo(Enum): A = 1 B = 2 - C = 4 + C = 3 x: Optional[Foo] if x: diff --git a/test-data/unit/check-expressions.test b/test-data/unit/check-expressions.test index d8eec99d78211..5d3d6b66d7b8a 100644 --- a/test-data/unit/check-expressions.test +++ b/test-data/unit/check-expressions.test @@ -323,8 +323,7 @@ if int(): b = b or a # E: Incompatible types in assignment (expression has type "Union[bool, A]", variable has type "bool") if int(): b = a or b # E: Incompatible types in assignment (expression has type "Union[A, bool]", variable has type "bool") -class A: - def __bool__(self) -> bool: pass +class A: pass [builtins fixtures/bool.pyi] @@ -376,8 +375,7 @@ if int(): d = c or d # E: Incompatible types in assignment (expression has type "C", variable has type "D") if int(): d = d or c # E: Incompatible types in assignment (expression has type "C", variable has type "D") -class C: - def __bool__(self) -> bool: pass +class C: pass class D(C): pass [builtins fixtures/bool.pyi] diff --git a/test-data/unit/check-inference-context.test b/test-data/unit/check-inference-context.test index 5a65b9749820d..82a412eeb3593 100644 --- a/test-data/unit/check-inference-context.test +++ b/test-data/unit/check-inference-context.test @@ -749,10 +749,8 @@ if int(): if int(): b = b or c # E: Incompatible types in assignment (expression has type "Union[List[B], List[C]]", variable has type "List[B]") -class A: - def __bool__(self) -> bool: ... -class B: - def __bool__(self) -> bool: ... +class A: pass +class B: pass class C(B): pass [builtins fixtures/list.pyi] diff --git a/test-data/unit/check-typevar-values.test b/test-data/unit/check-typevar-values.test index 8fb3b07e78a05..3f77996ec9596 100644 --- a/test-data/unit/check-typevar-values.test +++ b/test-data/unit/check-typevar-values.test @@ -86,9 +86,8 @@ class B: def g(self) -> B: return B() AB = TypeVar('AB', A, B) def f(x: AB) -> AB: - indeterminate: bool y = x - if indeterminate: + if y: return y.f() else: return y.g() # E: Incompatible return value type (got "B", expected "A") diff --git a/test-data/unit/fixtures/bool.pyi b/test-data/unit/fixtures/bool.pyi index 46b460c900bd5..cef2e9129fb70 100644 --- a/test-data/unit/fixtures/bool.pyi +++ b/test-data/unit/fixtures/bool.pyi @@ -10,11 +10,9 @@ class object: class type: pass class tuple(Generic[T]): pass class function: pass -class int: - def __bool__(self) -> 'bool': pass +class int: pass class bool(int): pass class float: pass -class str: - def __len__(self) -> int: pass +class str: pass class unicode: pass class ellipsis: pass diff --git a/test-data/unit/fixtures/dict.pyi b/test-data/unit/fixtures/dict.pyi index c165fbbeedfcb..ab8127badd4c3 100644 --- a/test-data/unit/fixtures/dict.pyi +++ b/test-data/unit/fixtures/dict.pyi @@ -33,7 +33,6 @@ class dict(Mapping[KT, VT]): class int: # for convenience def __add__(self, x: int) -> int: pass - def __bool__(self) -> bool: pass class str: pass # for keyword argument key type class unicode: pass # needed for py2 docstrings diff --git a/test-data/unit/fixtures/isinstancelist.pyi b/test-data/unit/fixtures/isinstancelist.pyi index 9dd12160fc451..fcc3032183aa9 100644 --- a/test-data/unit/fixtures/isinstancelist.pyi +++ b/test-data/unit/fixtures/isinstancelist.pyi @@ -18,13 +18,11 @@ def issubclass(x: object, t: Union[type, Tuple]) -> bool: pass class int: def __add__(self, x: int) -> int: pass - def __bool__(self) -> bool: pass class float: pass class bool(int): pass class str: def __add__(self, x: str) -> str: pass def __getitem__(self, x: int) -> str: pass - def __len__(self) -> int: pass T = TypeVar('T') KT = TypeVar('KT') diff --git a/test-data/unit/fixtures/ops.pyi b/test-data/unit/fixtures/ops.pyi index 694263a09b57f..d5845aba43c63 100644 --- a/test-data/unit/fixtures/ops.pyi +++ b/test-data/unit/fixtures/ops.pyi @@ -24,13 +24,12 @@ class tuple(Sequence[Tco]): class function: pass -class bool(int): pass +class bool: pass class str: def __init__(self, x: 'int') -> None: pass def __add__(self, x: 'str') -> 'str': pass def __eq__(self, x: object) -> bool: pass - def __len__(self) -> int: pass def startswith(self, x: 'str') -> bool: pass def strip(self) -> 'str': pass @@ -56,7 +55,6 @@ class int: def __le__(self, x: 'int') -> bool: pass def __gt__(self, x: 'int') -> bool: pass def __ge__(self, x: 'int') -> bool: pass - def __bool__(self) -> bool: pass class float: def __add__(self, x: 'float') -> 'float': pass diff --git a/test-data/unit/fixtures/primitives.pyi b/test-data/unit/fixtures/primitives.pyi index eec8a554de78a..71f59a9c1d8cf 100644 --- a/test-data/unit/fixtures/primitives.pyi +++ b/test-data/unit/fixtures/primitives.pyi @@ -17,7 +17,6 @@ class int: def __init__(self, x: object = ..., base: int = ...) -> None: pass def __add__(self, i: int) -> int: pass def __rmul__(self, x: int) -> int: pass - def __bool__(self) -> bool: pass class float: def __float__(self) -> float: pass class complex: pass diff --git a/test-data/unit/lib-stub/builtins.pyi b/test-data/unit/lib-stub/builtins.pyi index 96cd6dd66427d..57fc4b8f0de29 100644 --- a/test-data/unit/lib-stub/builtins.pyi +++ b/test-data/unit/lib-stub/builtins.pyi @@ -11,14 +11,11 @@ class type: # These are provided here for convenience. class int: def __add__(self, other: int) -> int: pass - def __bool__(self) -> bool: pass class bool(int): pass class float: pass -class str: - def __len__(self) -> int: pass -class bytes: - def __len__(self) -> int: pass +class str: pass +class bytes: pass class function: pass class ellipsis: pass diff --git a/test-data/unit/lib-stub/enum.pyi b/test-data/unit/lib-stub/enum.pyi index 2331071c4cd24..2c1b03532a5bc 100644 --- a/test-data/unit/lib-stub/enum.pyi +++ b/test-data/unit/lib-stub/enum.pyi @@ -35,7 +35,6 @@ def unique(enumeration: _T) -> _T: pass class Flag(Enum): def __or__(self: _T, other: Union[int, _T]) -> _T: pass - def __bool__(self) -> bool: ... class IntFlag(int, Flag): diff --git a/test-data/unit/lib-stub/typing.pyi b/test-data/unit/lib-stub/typing.pyi index 6f0d86a96cf01..2f42633843e0e 100644 --- a/test-data/unit/lib-stub/typing.pyi +++ b/test-data/unit/lib-stub/typing.pyi @@ -42,7 +42,6 @@ class Generator(Iterator[T], Generic[T, U, V]): class Sequence(Iterable[T_co]): def __getitem__(self, n: Any) -> T_co: pass - def __len__(self) -> int: pass class Mapping(Generic[T, T_co]): pass From d43222060cc6218eeaa5a9b0c00e7edfa3de08e4 Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Wed, 4 Aug 2021 23:35:11 -0400 Subject: [PATCH 26/31] confused but removing timedelta.__bool__ --- mypy/typeshed/stdlib/datetime.pyi | 1 - 1 file changed, 1 deletion(-) diff --git a/mypy/typeshed/stdlib/datetime.pyi b/mypy/typeshed/stdlib/datetime.pyi index 8462e80e2a598..e82b269235714 100644 --- a/mypy/typeshed/stdlib/datetime.pyi +++ b/mypy/typeshed/stdlib/datetime.pyi @@ -180,7 +180,6 @@ class timedelta(SupportsAbs[timedelta]): def __ge__(self, other: timedelta) -> bool: ... def __gt__(self, other: timedelta) -> bool: ... def __hash__(self) -> int: ... - def __bool__(self) -> bool: ... class datetime(date): min: ClassVar[datetime] From 6267456fd896f025d06a67a54a4797257fd6eb2f Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Wed, 4 Aug 2021 22:02:08 -0700 Subject: [PATCH 27/31] Apply suggestions from code review --- mypy/checker.py | 10 +++++----- test-data/unit/check-errorcodes.test | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/mypy/checker.py b/mypy/checker.py index f9c18a365ca95..50ff1c9aea654 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4012,7 +4012,7 @@ def _check_for_truthy_type(self, t: Type, expr: Expression) -> None: def format_expr_type() -> str: if isinstance(expr, MemberExpr): - return f'member "{expr.name}" has type "{t}"' + return f'Member "{expr.name}" has type "{t}"' elif isinstance(expr, RefExpr) and expr.fullname: return f'"{expr.fullname}" has type "{t}"' elif isinstance(expr, CallExpr): @@ -4020,18 +4020,18 @@ def format_expr_type() -> str: return f'"{expr.callee.name}" returns "{t}"' elif isinstance(expr.callee, RefExpr) and expr.callee.fullname: return f'"{expr.callee.fullname}" returns "{t}"' - return f'call returns "{t}"' + return f'Call returns "{t}"' else: - return f'expression has type "{t}"' + return f'Expression has type "{t}"' if isinstance(t, FunctionLike): self.msg.fail( - f'function "{t}" will always be true in boolean context', expr, + f'Function "{t}" will always be true in boolean context', expr, code=codes.IMPLICIT_BOOL, ) elif isinstance(t, UnionType): self.msg.fail( - f"{format_expr_type()} none of which implement __bool__ or __len__ " + f"{format_expr_type()} of which no members implement __bool__ or __len__ " "so it will always be true in boolean context", expr, code=codes.IMPLICIT_BOOL, diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 8cdd5f97c3ae0..f288cbf28be9d 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -844,9 +844,9 @@ if bad_union: # E: "__main__.bad_union" has type "Union[__main__.Foo, builtins. def f(): pass -if f: # E: function "def () -> Any" will always be true in boolean context [implicit-bool] +if f: # E: Function "def () -> Any" will always be true in boolean context [implicit-bool] pass -conditional_result = 'foo' if f else 'bar' # E: function "def () -> Any" will always be true in boolean context [implicit-bool] +conditional_result = 'foo' if f else 'bar' # E: Function "def () -> Any" will always be true in boolean context [implicit-bool] lst: List[int] = [] if lst: From 31977ab6e162114727b2a209e671d8029df3df00 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Wed, 4 Aug 2021 22:02:30 -0700 Subject: [PATCH 28/31] Update test-data/unit/check-errorcodes.test --- test-data/unit/check-errorcodes.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index f288cbf28be9d..cff8786c71067 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -839,7 +839,7 @@ if good_union: pass bad_union: Union[Foo, object] = Foo() -if bad_union: # E: "__main__.bad_union" has type "Union[__main__.Foo, builtins.object]" none of which implement __bool__ or __len__ so it will always be true in boolean context [implicit-bool] +if bad_union: # E: "__main__.bad_union" has type "Union[__main__.Foo, builtins.object]" of which no members implement __bool__ or __len__ so it will always be true in boolean context [implicit-bool] pass def f(): From 5c6d763e80246814dd6ac8833f812ebf7b483690 Mon Sep 17 00:00:00 2001 From: hauntsaninja <> Date: Sat, 7 Aug 2021 18:21:30 -0700 Subject: [PATCH 29/31] more copy changes --- docs/source/error_code_list2.rst | 7 ++++--- mypy/checker.py | 12 ++++++------ mypy/errorcodes.py | 6 +++--- test-data/unit/check-errorcodes.test | 10 +++++----- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index 7cf2997f14740..732148df3da9e 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -216,17 +216,18 @@ mypy generates an error if it thinks that an expression is redundant. [i for i in range(x) if isinstance(i, int)] -Check that expression is not implicitly true in boolean context [implicit-bool] +Check that expression is not implicitly true in boolean context [truthy-bool] -------------------------------------------------------------------------------- Warn when an expression whose type does not implement __bool__ or __len__ is used in boolean context -since it would always evaluate as true. +since it could always evaluate to true (if one also assumes that no subtype implements __bool__ or +__len__). .. code-block:: python class Foo: pass foo = Foo() - # Error: "foo" has type "Foo" which does not implement __bool__ or __len__ so it will always be true in boolean context + # Error: "foo" has type "Foo" which does not implement __bool__ or __len__ so it could always be true in boolean context if foo: ... diff --git a/mypy/checker.py b/mypy/checker.py index 50ff1c9aea654..9b2a192f09c55 100644 --- a/mypy/checker.py +++ b/mypy/checker.py @@ -4026,22 +4026,22 @@ def format_expr_type() -> str: if isinstance(t, FunctionLike): self.msg.fail( - f'Function "{t}" will always be true in boolean context', expr, - code=codes.IMPLICIT_BOOL, + f'Function "{t}" could always be true in boolean context', expr, + code=codes.TRUTHY_BOOL, ) elif isinstance(t, UnionType): self.msg.fail( f"{format_expr_type()} of which no members implement __bool__ or __len__ " - "so it will always be true in boolean context", + "so it could always be true in boolean context", expr, - code=codes.IMPLICIT_BOOL, + code=codes.TRUTHY_BOOL, ) else: self.msg.fail( f'{format_expr_type()} which does not implement __bool__ or __len__ ' - 'so it will always be true in boolean context', + 'so it could always be true in boolean context', expr, - code=codes.IMPLICIT_BOOL, + code=codes.TRUTHY_BOOL, ) def find_type_equals_check(self, node: ComparisonExpr, expr_indices: List[int] diff --git a/mypy/errorcodes.py b/mypy/errorcodes.py index a25b8b9bd68a2..d9e11a044a18d 100644 --- a/mypy/errorcodes.py +++ b/mypy/errorcodes.py @@ -122,9 +122,9 @@ def __str__(self) -> str: REDUNDANT_EXPR: Final = ErrorCode( "redundant-expr", "Warn about redundant expressions", "General", default_enabled=False ) -IMPLICIT_BOOL: Final = ErrorCode( - 'implicit-bool', - "Warn about potential use of implicit bool (that always returns true)", +TRUTHY_BOOL: Final = ErrorCode( + 'truthy-bool', + "Warn about expressions that could always evaluate to true in boolean contexts", 'General', default_enabled=False ) diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index cff8786c71067..4f4dc963b9b31 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -808,14 +808,14 @@ from typing_extensions import TypedDict Foo = TypedDict("Bar", {}) # E: First argument "Bar" to TypedDict() does not match variable name "Foo" [name-match] [builtins fixtures/dict.pyi] [case testImplicitBool] -# flags: --enable-error-code implicit-bool +# flags: --enable-error-code truthy-bool from typing import List, Union class Foo: pass foo = Foo() -if foo: # E: "__main__.foo" has type "__main__.Foo" which does not implement __bool__ or __len__ so it will always be true in boolean context [implicit-bool] +if foo: # E: "__main__.foo" has type "__main__.Foo" which does not implement __bool__ or __len__ so it could always be true in boolean context [truthy-bool] pass zero = 0 @@ -839,14 +839,14 @@ if good_union: pass bad_union: Union[Foo, object] = Foo() -if bad_union: # E: "__main__.bad_union" has type "Union[__main__.Foo, builtins.object]" of which no members implement __bool__ or __len__ so it will always be true in boolean context [implicit-bool] +if bad_union: # E: "__main__.bad_union" has type "Union[__main__.Foo, builtins.object]" of which no members implement __bool__ or __len__ so it could always be true in boolean context [truthy-bool] pass def f(): pass -if f: # E: Function "def () -> Any" will always be true in boolean context [implicit-bool] +if f: # E: Function "def () -> Any" could always be true in boolean context [truthy-bool] pass -conditional_result = 'foo' if f else 'bar' # E: Function "def () -> Any" will always be true in boolean context [implicit-bool] +conditional_result = 'foo' if f else 'bar' # E: Function "def () -> Any" could always be true in boolean context [truthy-bool] lst: List[int] = [] if lst: From 13af1eb530eea89d0193e345b92f833105d3a894 Mon Sep 17 00:00:00 2001 From: hauntsaninja <> Date: Sat, 7 Aug 2021 18:30:16 -0700 Subject: [PATCH 30/31] add a test --- test-data/unit/check-errorcodes.test | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test-data/unit/check-errorcodes.test b/test-data/unit/check-errorcodes.test index 4f4dc963b9b31..c2502a3533e81 100644 --- a/test-data/unit/check-errorcodes.test +++ b/test-data/unit/check-errorcodes.test @@ -807,7 +807,7 @@ from typing_extensions import TypedDict Foo = TypedDict("Bar", {}) # E: First argument "Bar" to TypedDict() does not match variable name "Foo" [name-match] [builtins fixtures/dict.pyi] -[case testImplicitBool] +[case testTruthyBool] # flags: --enable-error-code truthy-bool from typing import List, Union @@ -837,15 +837,21 @@ if s: good_union: Union[str, int] = 5 if good_union: pass +if not good_union: + pass bad_union: Union[Foo, object] = Foo() if bad_union: # E: "__main__.bad_union" has type "Union[__main__.Foo, builtins.object]" of which no members implement __bool__ or __len__ so it could always be true in boolean context [truthy-bool] pass +if not bad_union: # E: "__main__.bad_union" has type "builtins.object" which does not implement __bool__ or __len__ so it could always be true in boolean context [truthy-bool] + pass def f(): pass if f: # E: Function "def () -> Any" could always be true in boolean context [truthy-bool] pass +if not f: # E: Function "def () -> Any" could always be true in boolean context [truthy-bool] + pass conditional_result = 'foo' if f else 'bar' # E: Function "def () -> Any" could always be true in boolean context [truthy-bool] lst: List[int] = [] From 338e35b56a1e2b7fd72df9596ea003214e4e90ec Mon Sep 17 00:00:00 2001 From: Ilya Konstantinov Date: Thu, 2 Sep 2021 23:07:21 -0400 Subject: [PATCH 31/31] Try to provide more color in the docs --- docs/source/error_code_list2.rst | 33 ++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/docs/source/error_code_list2.rst b/docs/source/error_code_list2.rst index 732148df3da9e..578019cebec10 100644 --- a/docs/source/error_code_list2.rst +++ b/docs/source/error_code_list2.rst @@ -217,17 +217,42 @@ mypy generates an error if it thinks that an expression is redundant. Check that expression is not implicitly true in boolean context [truthy-bool] --------------------------------------------------------------------------------- +----------------------------------------------------------------------------- -Warn when an expression whose type does not implement __bool__ or __len__ is used in boolean context -since it could always evaluate to true (if one also assumes that no subtype implements __bool__ or -__len__). +Warn when an expression whose type does not implement ``__bool__`` or ``__len__`` is used in boolean context, +since unless implemented by a sub-type, the expression will always evaluate to true. .. code-block:: python + # mypy: enable-error-code truthy-bool + class Foo: pass foo = Foo() # Error: "foo" has type "Foo" which does not implement __bool__ or __len__ so it could always be true in boolean context if foo: ... + + +This check might falsely imply an error. For example, ``typing.Iterable`` does not implement +``__len__`` and so this code will be flagged: + +.. code-block:: python + + # mypy: enable-error-code truthy-bool + + def transform(items: Iterable[int]) -> List[int]: + # Error: "items" has type "typing.Iterable[int]" which does not implement __bool__ or __len__ so it could always be true in boolean context [truthy-bool] + if not items: + return [42] + return [x + 1 for x in items] + + + +If called as ``transform((int(s) for s in []))``, this function would not return ``[42]]`` unlike what the author +might have intended. Of course it's possible that ``transform`` is only passed ``list`` objects, and so there is +no error in practice. In such case, it might be prudent to annotate ``items: typing.Sequence[int]``. + +This is similar in concept to ensuring that an expression's type implements an expected interface (e.g. ``typing.Sized``), +except that attempting to invoke an undefined method (e.g. ``__len__``) results in an error, +while attempting to evaluate an object in boolean context without a concrete implementation results in a truthy value.