Skip to content

Commit b4986aa

Browse files
authored
fix(b909): Fix false positive affecting containers of mutables (#469)
* fix(b909): Fix false positive affecting containers of mutables The false positives occurred when trying to edit a dictionary while iterating over a list of dictionaries: ``` lst: list[dict] = [{}, {}, {}] for dic in lst: dic["key"] = False # was false positive - fixed now ``` * fix(b909): Allow mutation of dict[key] form These changes allow the following: ``` some_dict = {"foo": "bar"} for key in some_dict: some_dict[key] = 3 # no error (previously error'd) ``` * fix(b909): Fix python 3.8 incompatibility Turns out, that the slice type was changed in python 3.9. > Changed in version 3.9: Simple indices are represented by their value, > extended slices are represented as tuples. from https://docs.python.org/3/library/ast.html#module-ast
1 parent b9f9dce commit b4986aa

File tree

2 files changed

+73
-3
lines changed

2 files changed

+73
-3
lines changed

bugbear.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1581,11 +1581,13 @@ def check(num_args, param_name):
15811581
def check_for_b909(self, node: ast.For):
15821582
if isinstance(node.iter, ast.Name):
15831583
name = _to_name_str(node.iter)
1584+
key = _to_name_str(node.target)
15841585
elif isinstance(node.iter, ast.Attribute):
15851586
name = _to_name_str(node.iter)
1587+
key = _to_name_str(node.target)
15861588
else:
15871589
return
1588-
checker = B909Checker(name)
1590+
checker = B909Checker(name, key)
15891591
checker.visit(node.body)
15901592
for mutation in itertools.chain.from_iterable(
15911593
m for m in checker.mutations.values()
@@ -1603,6 +1605,46 @@ def compose_call_path(node):
16031605
yield node.id
16041606

16051607

1608+
def _tansform_slice_to_py39(slice: ast.Slice) -> ast.Slice | ast.Name:
1609+
"""Transform a py38 style slice to a py39 style slice.
1610+
1611+
In py39 the slice was changed to have simple names directly assigned:
1612+
```py
1613+
# code:
1614+
some_dict[key]
1615+
# py38:
1616+
slice=Index(
1617+
value=Name(
1618+
lineno=152,
1619+
col_offset=14,
1620+
end_lineno=152,
1621+
end_col_offset=17,
1622+
id='key',
1623+
ctx=Load()
1624+
),
1625+
)
1626+
# py39 onwards:
1627+
slice=Name(
1628+
lineno=152,
1629+
col_offset=14,
1630+
end_lineno=152,
1631+
end_col_offset=17,
1632+
id='key',
1633+
ctx=Load()
1634+
),
1635+
```
1636+
1637+
> Changed in version 3.9: Simple indices are represented by their value,
1638+
> extended slices are represented as tuples.
1639+
from https://docs.python.org/3/library/ast.html#module-ast
1640+
"""
1641+
if sys.version_info >= (3, 9):
1642+
return slice
1643+
if isinstance(slice, ast.Index) and isinstance(slice.value, ast.Name):
1644+
slice = slice.value
1645+
return slice
1646+
1647+
16061648
class B909Checker(ast.NodeVisitor):
16071649
# https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types
16081650
MUTATING_FUNCTIONS = (
@@ -1624,14 +1666,19 @@ class B909Checker(ast.NodeVisitor):
16241666
"discard",
16251667
)
16261668

1627-
def __init__(self, name: str):
1669+
def __init__(self, name: str, key: str):
16281670
self.name = name
1671+
self.key = key
16291672
self.mutations = defaultdict(list)
16301673
self._conditional_block = 0
16311674

16321675
def visit_Assign(self, node: ast.Assign):
16331676
for target in node.targets:
1634-
if isinstance(target, ast.Subscript) and _to_name_str(target.value):
1677+
if (
1678+
isinstance(target, ast.Subscript)
1679+
and _to_name_str(target.value) == self.name
1680+
and _to_name_str(_tansform_slice_to_py39(target.slice)) != self.key
1681+
):
16351682
self.mutations[self._conditional_block].append(node)
16361683
self.generic_visit(node)
16371684

tests/b909.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,26 @@ def __init__(self, ls):
127127
bar.remove(1)
128128
break
129129
break
130+
131+
lst: list[dict] = [{}, {}, {}]
132+
for dic in lst:
133+
dic["key"] = False # no error
134+
135+
136+
137+
for grammar in grammars:
138+
errors[grammar.version] = InvalidInput() # no error
139+
140+
141+
142+
for key in self.hpo_params:
143+
if key in nni_config:
144+
nni_config[key] = self.hpo_params[key] # no error
145+
146+
147+
some_dict = {"foo": "bar"}
148+
for key in some_dict:
149+
some_dict[key] = 3 # no error
150+
151+
for key in some_dict.keys():
152+
some_dict[key] = 3 # no error

0 commit comments

Comments
 (0)