Skip to content

Commit

Permalink
Add error-code for truthy-iterable
Browse files Browse the repository at this point in the history
  • Loading branch information
cdce8p committed Sep 29, 2022
1 parent 3015abf commit 412a3a9
Show file tree
Hide file tree
Showing 7 changed files with 43 additions and 19 deletions.
28 changes: 13 additions & 15 deletions docs/source/error_code_list2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -231,31 +231,29 @@ since unless implemented by a sub-type, the expression will always evaluate to t
if foo:
...
This is similar in concept to ensuring that an expression's type implements an expected interface (e.g. ``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.

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

.. code-block:: python
Check that iterable is not implicitly true in boolean context [truthy-iterable]
-------------------------------------------------------------------------------

``Iterable`` does not implement ``__len__`` and so this code will be flagged:

# Use "mypy -enable-error-code truthy-bool ..."
.. code-block:: python
from typing import Iterable
def transform(items: Iterable[int]) -> Iterable[int]:
# Error: "items" has type "Iterable[int]" which does not implement __bool__ or __len__ so it could always be true in boolean context [truthy-bool]
def transform(items: Iterable[int]) -> list[int]:
# Error :"items" has type "Iterable[int]" which can always be true in boolean context. Consider using "Collection[int]" instead. [truthy-iterable]
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: Sequence[int]``.

This is similar in concept to ensuring that an expression's type implements an expected interface (e.g. ``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.
If called with a ``Generator`` like ``int(x) for x 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 is recommended to annotate ``items: Collection[int]``.


.. _ignore-without-code:
Expand Down
8 changes: 8 additions & 0 deletions mypy/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4989,6 +4989,14 @@ def format_expr_type() -> str:
self.fail(message_registry.FUNCTION_ALWAYS_TRUE.format(format_type(t)), expr)
elif isinstance(t, UnionType):
self.fail(message_registry.TYPE_ALWAYS_TRUE_UNIONTYPE.format(format_expr_type()), expr)
elif isinstance(t, Instance) and t.type.fullname == "typing.Iterable":
_, info = self.make_fake_typeinfo("typing", "Collection", "Collection", [])
self.fail(
message_registry.ITERABLE_ALWAYS_TRUE.format(
format_expr_type(), format_type(Instance(info, t.args))
),
expr,
)
else:
self.fail(message_registry.TYPE_ALWAYS_TRUE.format(format_expr_type()), expr)

Expand Down
5 changes: 5 additions & 0 deletions mypy/errorcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ def __str__(self) -> str:
"General",
default_enabled=False,
)
TRUTHY_ITERABLE: Final[ErrorCode] = ErrorCode(
"truthy-iterable",
"Warn about Iterable expressions that could always evaluate to true in boolean contexts",
"General",
)
NAME_MATCH: Final = ErrorCode(
"name-match", "Check that type definition has consistent naming", "General"
)
Expand Down
4 changes: 4 additions & 0 deletions mypy/message_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,10 @@ def with_additional_msg(self, info: str) -> ErrorMessage:
FUNCTION_ALWAYS_TRUE: Final = ErrorMessage(
"Function {} could always be true in boolean context", code=codes.TRUTHY_BOOL
)
ITERABLE_ALWAYS_TRUE: Final = ErrorMessage(
"{} which can always be true in boolean context. Consider using {} instead.",
code=codes.TRUTHY_ITERABLE,
)
NOT_CALLABLE: Final = "{} not callable"
TYPE_MUST_BE_USED: Final = "Value of type {} must be used"

Expand Down
6 changes: 4 additions & 2 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
from __future__ import annotations

from contextlib import contextmanager
from typing import Any, Callable, Iterable, Iterator, List, TypeVar, cast
from typing import Any, Callable, Collection, Iterable, Iterator, List, TypeVar, cast
from typing_extensions import Final, TypeAlias as _TypeAlias

from mypy import errorcodes as codes, message_registry
Expand Down Expand Up @@ -6115,7 +6115,9 @@ def add_plugin_dependency(self, trigger: str, target: str | None = None) -> None
target = self.scope.current_target()
self.cur_mod_node.plugin_deps.setdefault(trigger, set()).add(target)

def add_type_alias_deps(self, aliases_used: Iterable[str], target: str | None = None) -> None:
def add_type_alias_deps(
self, aliases_used: Collection[str], target: str | None = None
) -> None:
"""Add full names of type aliases on which the current node depends.
This is used by fine-grained incremental mode to re-check the corresponding nodes.
Expand Down
4 changes: 2 additions & 2 deletions mypyc/test-data/irbuild-statements.test
Original file line number Diff line number Diff line change
Expand Up @@ -1006,9 +1006,9 @@ L5:
return 1

[case testForZip]
from typing import List, Iterable
from typing import List, Iterable, Sequence

def f(a: List[int], b: Iterable[bool]) -> None:
def f(a: List[int], b: Sequence[bool]) -> None:
for x, y in zip(a, b):
if b:
x = 1
Expand Down
7 changes: 7 additions & 0 deletions test-data/unit/check-errorcodes.test
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,13 @@ if lst:
pass
[builtins fixtures/list.pyi]

[case testTruthyIterable]
# flags: --strict-optional
from typing import Iterable
def func(var: Iterable[str]) -> None:
if var: # E: "var" has type "Iterable[str]" which can always be true in boolean context. Consider using "Collection[str]" instead. [truthy-iterable]
...

[case testNoOverloadImplementation]
from typing import overload

Expand Down

0 comments on commit 412a3a9

Please sign in to comment.