Skip to content

Commit 7196efa

Browse files
authored
implement B035 check for static keys in dict-comprehension (#426)
* implement B035 check for static keys in dict-comprehension * rekick CI
1 parent cfc2429 commit 7196efa

File tree

4 files changed

+84
-0
lines changed

4 files changed

+84
-0
lines changed

README.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ second usage. Save the result to a list if the result is needed multiple times.
190190

191191
**B034**: Calls to `re.sub`, `re.subn` or `re.split` should pass `flags` or `count`/`maxsplit` as keyword arguments. It is commonly assumed that `flags` is the third positional parameter, forgetting about `count`/`maxsplit`, since many other `re` module functions are of the form `f(pattern, string, flags)`.
192192

193+
**B035**: Found dict comprehension with a static key - either a constant value or variable not from the comprehension expression. This will result in a dict with a single key that was repeatedly overwritten.
194+
193195
Opinionated warnings
194196
~~~~~~~~~~~~~~~~~~~~
195197

bugbear.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ def visit_SetComp(self, node):
483483

484484
def visit_DictComp(self, node):
485485
self.check_for_b023(node)
486+
self.check_for_b035(node)
486487
self.generic_visit(node)
487488

488489
def visit_GeneratorExp(self, node):
@@ -954,6 +955,36 @@ def check_for_b031(self, loop_node): # noqa: C901
954955
B031(node.lineno, node.col_offset, vars=(node.id,))
955956
)
956957

958+
def _get_names_from_tuple(self, node: ast.Tuple):
959+
for dim in node.elts:
960+
if isinstance(dim, ast.Name):
961+
yield dim.id
962+
elif isinstance(dim, ast.Tuple):
963+
yield from self._get_names_from_tuple(dim)
964+
965+
def _get_dict_comp_loop_var_names(self, node: ast.DictComp):
966+
for gen in node.generators:
967+
if isinstance(gen.target, ast.Name):
968+
yield gen.target.id
969+
elif isinstance(gen.target, ast.Tuple):
970+
yield from self._get_names_from_tuple(gen.target)
971+
972+
def check_for_b035(self, node: ast.DictComp):
973+
"""Check that a static key isn't used in a dict comprehension.
974+
975+
Emit a warning if a likely unchanging key is used - either a constant,
976+
or a variable that isn't coming from the generator expression.
977+
"""
978+
if isinstance(node.key, ast.Constant):
979+
self.errors.append(
980+
B035(node.key.lineno, node.key.col_offset, vars=(node.key.value,))
981+
)
982+
elif isinstance(node.key, ast.Name):
983+
if node.key.id not in self._get_dict_comp_loop_var_names(node):
984+
self.errors.append(
985+
B035(node.key.lineno, node.key.col_offset, vars=(node.key.id,))
986+
)
987+
957988
def _get_assigned_names(self, loop_node):
958989
loop_targets = (ast.For, ast.AsyncFor, ast.comprehension)
959990
for node in children_in_scope(loop_node):
@@ -1884,6 +1915,8 @@ def visit_Lambda(self, node):
18841915
" due to unintuitive argument positions."
18851916
)
18861917
)
1918+
B035 = Error(message="B035 Static key in dict comprehension {!r}.")
1919+
18871920

18881921
# Warnings disabled by default.
18891922
B901 = Error(

tests/b035.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# OK - consts in regular dict
2+
regular_dict = {"a": 1, "b": 2}
3+
regular_nested_dict = {"a": 1, "nested": {"b": 2, "c": "three"}}
4+
5+
# bad - const key in dict comprehension
6+
bad_const_key_str = {"a": i for i in range(3)}
7+
bad_const_key_int = {1: i for i in range(3)}
8+
9+
# OK - const value in dict comp
10+
const_val = {i: "a" for i in range(3)}
11+
12+
# OK - expression with const in dict comp
13+
key_expr_with_const = {i * i: i for i in range(3)}
14+
key_expr_with_const2 = {"a" * i: i for i in range(3)}
15+
16+
# nested
17+
nested_bad_and_good = {
18+
"good": {"a": 1, "b": 2},
19+
"bad": {"a": i for i in range(3)},
20+
}
21+
22+
CONST_KEY_VAR = "KEY"
23+
24+
# bad
25+
bad_const_key_var = {CONST_KEY_VAR: i for i in range(3)}
26+
27+
# OK - variable from tuple
28+
var_from_tuple = {k: v for k, v in {}.items()}
29+
30+
# OK - variable from nested tuple
31+
var_from_nested_tuple = {v2: k for k, (v1, v2) in {"a": (1, 2)}.items()}
32+
33+
# bad - variabe not from generator
34+
v3 = 1
35+
bad_var_not_from_nested_tuple = {v3: k for k, (v1, v2) in {"a": (1, 2)}.items()}

tests/test_bugbear.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
B032,
4444
B033,
4545
B034,
46+
B035,
4647
B901,
4748
B902,
4849
B903,
@@ -521,6 +522,19 @@ def test_b034(self):
521522
)
522523
self.assertEqual(errors, expected)
523524

525+
def test_b035(self):
526+
filename = Path(__file__).absolute().parent / "b035.py"
527+
bbc = BugBearChecker(filename=str(filename))
528+
errors = list(bbc.run())
529+
expected = self.errors(
530+
B035(6, 21, vars=("a",)),
531+
B035(7, 21, vars=(1,)),
532+
B035(19, 12, vars=("a",)),
533+
B035(25, 21, vars=("CONST_KEY_VAR",)),
534+
B035(35, 33, vars=("v3",)),
535+
)
536+
self.assertEqual(errors, expected)
537+
524538
def test_b908(self):
525539
filename = Path(__file__).absolute().parent / "b908.py"
526540
bbc = BugBearChecker(filename=str(filename))

0 commit comments

Comments
 (0)