Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -178,10 +178,13 @@ The check also considers function calls without side-effects such as ``isinstanc
unary and arithmetic operations such as ``-x`` or ``a * b``, generator expressions
(which are lazy, so their body never runs) and lambdas (which nothing can call), as
long as what they evaluate right away doesn't call a function, like
``(x for x in f())`` or ``lambda x=f(): x``. Shifts, bitwise operators and ``@`` are
left alone, since libraries overload them for their side effects, and so is an
operation, a generator expression or a lambda with defaults directly in a ``try`` or
``with`` block, where it may be run only to see it raise.
``(x for x in f())`` or ``lambda x=f(): x``. Calls to ``map``, ``filter``,
``reversed``, ``zip`` and ``enumerate`` are reported too: they are lazy, so in
``map(print, items)`` on its own line ``print`` is never called. Shifts, bitwise
operators and ``@`` are left alone, since libraries overload them for their side
effects, and so is an operation, a generator expression, a lambda with defaults or a
call to one of these lazy builtins directly in a ``try`` or ``with`` block, where it
may be run only to see it raise.
Note that dangling commas will cause things to be interpreted as useless tuples.
For example, in the statement ``print(".."),`` is the same as ``(print(".."),)``
which is an unassigned tuple. Simply remove the comma to clear the error.
Expand Down Expand Up @@ -605,6 +608,9 @@ UNRELEASED
* B018: also report generator expressions and lambdas used as statements. A
statement like ``(print(x) for x in items)`` never runs its body, and a bare
``lambda x: True`` is usually an assignment that lost its target (#452)
* B018: also report ``map()``, ``filter()``, ``reversed()``, ``zip()`` and
``enumerate()`` calls used as statements. They return lazy iterators, so
``map(print, items)`` on its own line never calls ``print`` (#452)

26.9.30
~~~~~~~
Expand Down
16 changes: 14 additions & 2 deletions bugbear.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@
ast.Mod,
ast.Pow,
)
# Builtins that return a lazy iterator, so `map(print, items)` as a statement
# never calls `print`. `iter` is left out: `iter(x)` calls `x.__iter__()` right
# away, which is what a bare `iter(x)` is usually for.
B018_LAZY_FUNCTIONS = ("enumerate", "filter", "map", "reversed", "zip")
B018_RAISING_BLOCKS: tuple[type[ast.stmt], ...] = (ast.Try, ast.With, ast.AsyncWith)
if sys.version_info >= (3, 11):
B018_RAISING_BLOCKS += (ast.TryStar,)
Expand Down Expand Up @@ -2501,12 +2505,20 @@ def check_for_b018(self, node: ast.AST) -> None:
map(_is_useless_operation, _lambda_defaults(node.value))
)
)
# `map()`, `filter()` and the like are lazy too, so the
# function or loop they were meant for never runs
or (
isinstance(node.value, ast.Call)
and isinstance(node.value.func, ast.Name)
and node.value.func.id in B018_LAZY_FUNCTIONS
)
)
# directly in a `try` or `with` block, an operation is often run
# only to see it raise: `with pytest.raises(TypeError): "1" + 1`.
# The same goes for the first iterable of a generator expression,
# which `iter()` is called on, and the defaults of a lambda:
# `(x for x in 1)` or `lambda x=1 / 0: x`
# which `iter()` is called on, the defaults of a lambda, and the
# arguments of a lazy builtin, which it checks right away:
# `(x for x in 1)`, `lambda x=1 / 0: x` or `zip(1)`
and not (
self.node_stack
and isinstance(self.node_stack[-1], B018_RAISING_BLOCKS)
Expand Down
47 changes: 47 additions & 0 deletions tests/eval_files/b018_lazy_calls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""
Should emit:
B018 - on lines 11-18 and 21
"""


def discarded(items, names, values, handle, fetch):
# map(), filter(), reversed(), zip() and enumerate() return lazy iterators:
# as a statement, nothing ever iterates over them, so the function they get
# is never called (#452)
map(print, items) # B018: 4, "Call"
map(lambda item: item.save(), items) # B018: 4, "Call"
filter(handle, items) # B018: 4, "Call"
reversed(items) # B018: 4, "Call"
zip(names, values, strict=True) # B018: 4, "Call"
enumerate(items) # B018: 4, "Call"
# `fetch` is still called here, but `handle` never is
map(handle, fetch()) # B018: 4, "Call"
for item in items:
if item:
map(handle, item) # B018: 12, "Call"


def used(items, names, values, handle, pool):
list(map(print, items))
handled = map(handle, items)
for name, value in zip(names, values, strict=True):
handle(name, value)
pool.map(handle, items)
items.reverse()
# iter() calls `__iter__` right away
iter(items)
next(iter(items))
return handled, filter(handle, items), [reversed(items), enumerate(items)]


def expected_to_raise(raises, obj):
# directly in a `try` or `with` block, a lazy builtin may be called only to
# see it raise, since it checks its arguments right away
with raises(TypeError):
zip(1, strict=True)
with raises(TypeError):
map(print, obj)
try:
reversed(obj)
except TypeError:
pass
32 changes: 16 additions & 16 deletions tests/eval_files/b905_py310.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
zip() # B905: 0
zip(range(3)) # B905: 0
zip("a", "b") # B905: 0
zip("a", "b", *zip("c")) # B905: 0 # B905: 15
zip(zip("a"), strict=False) # B905: 4
zip(zip("a", strict=True)) # B905: 0
_ = zip() # B905: 4
_ = zip(range(3)) # B905: 4
_ = zip("a", "b") # B905: 4
_ = zip("a", "b", *zip("c")) # B905: 4 # B905: 19
_ = zip(zip("a"), strict=False) # B905: 8
_ = zip(zip("a", strict=True)) # B905: 4

zip(range(3), strict=True)
zip("a", "b", strict=False)
zip("a", "b", "c", strict=True)
_ = zip(range(3), strict=True)
_ = zip("a", "b", strict=False)
_ = zip("a", "b", "c", strict=True)

# infinite iterators from itertools module should not raise errors
import itertools

zip([1, 2, 3], itertools.cycle("ABCDEF"))
zip([1, 2, 3], itertools.count())
zip([1, 2, 3], itertools.repeat(1))
zip([1, 2, 3], itertools.repeat(1, None))
zip([1, 2, 3], itertools.repeat(1, times=None))
_ = zip([1, 2, 3], itertools.cycle("ABCDEF"))
_ = zip([1, 2, 3], itertools.count())
_ = zip([1, 2, 3], itertools.repeat(1))
_ = zip([1, 2, 3], itertools.repeat(1, None))
_ = zip([1, 2, 3], itertools.repeat(1, times=None))

zip([1, 2, 3], itertools.repeat(1, 1)) # B905: 0
zip([1, 2, 3], itertools.repeat(1, times=4)) # B905: 0
_ = zip([1, 2, 3], itertools.repeat(1, 1)) # B905: 4
_ = zip([1, 2, 3], itertools.repeat(1, times=4)) # B905: 4
14 changes: 7 additions & 7 deletions tests/eval_files/b912_py314.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
map(lambda x: x, "a", "b") # B912: 0
map(lambda x: x, "a", "b", *map("c")) # B912: 0
map(lambda x: x, "a", map(lambda x: x, "a"), strict=True)
_ = map(lambda x: x, "a", "b") # B912: 4
_ = map(lambda x: x, "a", "b", *map("c")) # B912: 4
_ = map(lambda x: x, "a", map(lambda x: x, "a"), strict=True)

map()
map(lambda x: x, "a")
map(lambda x: x, "a", "b", strict=False)
map(lambda x: x, "a", "b", "c", strict=True)
_ = map()
_ = map(lambda x: x, "a")
_ = map(lambda x: x, "a", "b", strict=False)
_ = map(lambda x: x, "a", "b", "c", strict=True)
Loading