Skip to content

Commit 23d5401

Browse files
committed
Add error-code for truthy-iterable
1 parent dbcbb3f commit 23d5401

File tree

7 files changed

+43
-19
lines changed

7 files changed

+43
-19
lines changed

docs/source/error_code_list2.rst

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -231,31 +231,29 @@ since unless implemented by a sub-type, the expression will always evaluate to t
231231
if foo:
232232
...
233233
234+
This is similar in concept to ensuring that an expression's type implements an expected interface (e.g. ``Sized``),
235+
except that attempting to invoke an undefined method (e.g. ``__len__``) results in an error,
236+
while attempting to evaluate an object in boolean context without a concrete implementation results in a truthy value.
234237

235-
This check might falsely imply an error. For example, ``Iterable`` does not implement
236-
``__len__`` and so this code will be flagged:
237238

238-
.. code-block:: python
239+
Check that iterable is not implicitly true in boolean context [truthy-iterable]
240+
-------------------------------------------------------------------------------
241+
242+
``Iterable`` does not implement ``__len__`` and so this code will be flagged:
239243

240-
# Use "mypy -enable-error-code truthy-bool ..."
244+
.. code-block:: python
241245
242246
from typing import Iterable
243247
244-
def transform(items: Iterable[int]) -> Iterable[int]:
245-
# Error: "items" has type "Iterable[int]" which does not implement __bool__ or __len__ so it could always be true in boolean context [truthy-bool]
248+
def transform(items: Iterable[int]) -> list[int]:
249+
# Error :"items" has type "Iterable[int]" which can always be true in boolean context. Consider using "Collection[int]" instead. [truthy-iterable]
246250
if not items:
247251
return [42]
248252
return [x + 1 for x in items]
249253
250-
251-
252-
If called as ``transform((int(s) for s in []))``, this function would not return ``[42]`` unlike what the author
253-
might have intended. Of course it's possible that ``transform`` is only passed ``list`` objects, and so there is
254-
no error in practice. In such case, it might be prudent to annotate ``items: Sequence[int]``.
255-
256-
This is similar in concept to ensuring that an expression's type implements an expected interface (e.g. ``Sized``),
257-
except that attempting to invoke an undefined method (e.g. ``__len__``) results in an error,
258-
while attempting to evaluate an object in boolean context without a concrete implementation results in a truthy value.
254+
If called with a ``Generator`` like ``int(x) for x in []``, this function would not return ``[42]`` unlike
255+
what the author might have intended. Of course it's possible that ``transform`` is only passed ``list`` objects,
256+
and so there is no error in practice. In such case, it is recommended to annotate ``items: Collection[int]``.
259257

260258

261259
Check that function isn't used in boolean context [truthy-function]

mypy/checker.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5088,6 +5088,14 @@ def format_expr_type() -> str:
50885088
self.fail(message_registry.FUNCTION_ALWAYS_TRUE.format(format_type(t)), expr)
50895089
elif isinstance(t, UnionType):
50905090
self.fail(message_registry.TYPE_ALWAYS_TRUE_UNIONTYPE.format(format_expr_type()), expr)
5091+
elif isinstance(t, Instance) and t.type.fullname == "typing.Iterable":
5092+
_, info = self.make_fake_typeinfo("typing", "Collection", "Collection", [])
5093+
self.fail(
5094+
message_registry.ITERABLE_ALWAYS_TRUE.format(
5095+
format_expr_type(), format_type(Instance(info, t.args))
5096+
),
5097+
expr,
5098+
)
50915099
else:
50925100
self.fail(message_registry.TYPE_ALWAYS_TRUE.format(format_expr_type()), expr)
50935101

mypy/errorcodes.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,11 @@ def __str__(self) -> str:
160160
"Warn about function that always evaluate to true in boolean contexts",
161161
"General",
162162
)
163+
TRUTHY_ITERABLE: Final[ErrorCode] = ErrorCode(
164+
"truthy-iterable",
165+
"Warn about Iterable expressions that could always evaluate to true in boolean contexts",
166+
"General",
167+
)
163168
NAME_MATCH: Final = ErrorCode(
164169
"name-match", "Check that type definition has consistent naming", "General"
165170
)

mypy/message_registry.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ def with_additional_msg(self, info: str) -> ErrorMessage:
153153
FUNCTION_ALWAYS_TRUE: Final = ErrorMessage(
154154
"Function {} could always be true in boolean context", code=codes.TRUTHY_FUNCTION
155155
)
156+
ITERABLE_ALWAYS_TRUE: Final = ErrorMessage(
157+
"{} which can always be true in boolean context. Consider using {} instead.",
158+
code=codes.TRUTHY_ITERABLE,
159+
)
156160
NOT_CALLABLE: Final = "{} not callable"
157161
TYPE_MUST_BE_USED: Final = "Value of type {} must be used"
158162

mypy/semanal.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
from __future__ import annotations
5252

5353
from contextlib import contextmanager
54-
from typing import Any, Callable, Iterable, Iterator, List, TypeVar, cast
54+
from typing import Any, Callable, Collection, Iterable, Iterator, List, TypeVar, cast
5555
from typing_extensions import Final, TypeAlias as _TypeAlias
5656

5757
from mypy import errorcodes as codes, message_registry
@@ -6202,7 +6202,9 @@ def add_plugin_dependency(self, trigger: str, target: str | None = None) -> None
62026202
target = self.scope.current_target()
62036203
self.cur_mod_node.plugin_deps.setdefault(trigger, set()).add(target)
62046204

6205-
def add_type_alias_deps(self, aliases_used: Iterable[str], target: str | None = None) -> None:
6205+
def add_type_alias_deps(
6206+
self, aliases_used: Collection[str], target: str | None = None
6207+
) -> None:
62066208
"""Add full names of type aliases on which the current node depends.
62076209
62086210
This is used by fine-grained incremental mode to re-check the corresponding nodes.

mypyc/test-data/irbuild-statements.test

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,9 +1006,9 @@ L5:
10061006
return 1
10071007

10081008
[case testForZip]
1009-
from typing import List, Iterable
1009+
from typing import List, Iterable, Sequence
10101010

1011-
def f(a: List[int], b: Iterable[bool]) -> None:
1011+
def f(a: List[int], b: Sequence[bool]) -> None:
10121012
for x, y in zip(a, b):
10131013
if b:
10141014
x = 1

test-data/unit/check-errorcodes.test

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -857,6 +857,13 @@ if not f: # E: Function "Callable[[], Any]" could always be true in boolean con
857857
pass
858858
conditional_result = 'foo' if f else 'bar' # E: Function "Callable[[], Any]" could always be true in boolean context [truthy-function]
859859

860+
[case testTruthyIterable]
861+
# flags: --strict-optional
862+
from typing import Iterable
863+
def func(var: Iterable[str]) -> None:
864+
if var: # E: "var" has type "Iterable[str]" which can always be true in boolean context. Consider using "Collection[str]" instead. [truthy-iterable]
865+
...
866+
860867
[case testNoOverloadImplementation]
861868
from typing import overload
862869

0 commit comments

Comments
 (0)