Skip to content

Commit 4cf3906

Browse files
committed
Implemented consider-using-dict-items on comprehension
- Also, added new test cases (thanks @cdce8p)
1 parent 141b7b7 commit 4cf3906

File tree

3 files changed

+112
-24
lines changed

3 files changed

+112
-24
lines changed

pylint/checkers/refactoring/recommendation_checker.py

Lines changed: 68 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,40 @@
11
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
22
# For details: https://github.com/PyCQA/pylint/blob/master/LICENSE
33

4-
from typing import cast
4+
from typing import Optional, Union, cast
55

66
import astroid
77

88
from pylint import checkers, interfaces
99
from pylint.checkers import utils
1010

1111

12+
def _check_if_dict_keys_used(
13+
node: Union[astroid.For, astroid.Comprehension]
14+
) -> Optional[str]:
15+
if not isinstance(node.iter, astroid.Call):
16+
# Is it a dictionary?
17+
if not isinstance(node.iter, (astroid.Name, astroid.Attribute)):
18+
return None
19+
inferred = utils.safe_infer(node.iter)
20+
if not isinstance(inferred, (astroid.Dict, astroid.DictComp)):
21+
return None
22+
iterating_object_name = node.iter.as_string()
23+
24+
else:
25+
# Is it a proper keys call?
26+
if (
27+
isinstance(node.iter.func, astroid.Name)
28+
or node.iter.func.attrname != "keys"
29+
):
30+
return None
31+
inferred = utils.safe_infer(node.iter.func)
32+
if not isinstance(inferred, (astroid.BoundMethod, astroid.Dict)):
33+
return None
34+
iterating_object_name = node.iter.as_string().partition(".")[0]
35+
return iterating_object_name
36+
37+
1238
class RecommendationChecker(checkers.BaseChecker):
1339

1440
__implements__ = (interfaces.IAstroidChecker,)
@@ -140,26 +166,9 @@ def _check_consider_using_dict_items(self, node: astroid.For) -> None:
140166
# that the object which is iterated is used as a subscript in the
141167
# body of the for.
142168

143-
if not isinstance(node.iter, astroid.Call):
144-
# Is it a dictionary?
145-
if not isinstance(node.iter, astroid.Name):
146-
return
147-
inferred = utils.safe_infer(node.iter)
148-
if not isinstance(inferred, (astroid.Dict, astroid.DictComp)):
149-
return
150-
iterating_object_name = node.iter.as_string()
151-
152-
else:
153-
# Is it a proper keys call?
154-
if not (
155-
isinstance(node.iter.func, astroid.Attribute)
156-
and node.iter.func.attrname != "keys"
157-
):
158-
return
159-
inferred = utils.safe_infer(node.iter.func)
160-
if not isinstance(inferred, (astroid.BoundMethod, astroid.Dict)):
161-
return
162-
iterating_object_name = node.iter.as_string().partition(".")[0]
169+
iterating_object_name = _check_if_dict_keys_used(node)
170+
if iterating_object_name is None:
171+
return
163172

164173
# Verify that the body of the for loop uses a subscript
165174
# with the object that was iterated. This uses some heuristics
@@ -168,7 +177,8 @@ def _check_consider_using_dict_items(self, node: astroid.For) -> None:
168177
for child in node.body:
169178
for subscript in child.nodes_of_class(astroid.Subscript):
170179
subscript = cast(astroid.Subscript, subscript)
171-
if not isinstance(subscript.value, astroid.Name):
180+
181+
if not isinstance(subscript.value, (astroid.Name, astroid.Attribute)):
172182
continue
173183

174184
value = subscript.slice
@@ -177,8 +187,9 @@ def _check_consider_using_dict_items(self, node: astroid.For) -> None:
177187
if (
178188
not isinstance(value, astroid.Name)
179189
or value.name != node.target.name
180-
or iterating_object_name != subscript.value.name
190+
or iterating_object_name != subscript.value.as_string()
181191
):
192+
182193
continue
183194
last_definition_lineno = value.lookup(value.name)[1][-1].lineno
184195
if last_definition_lineno > node.lineno:
@@ -187,5 +198,39 @@ def _check_consider_using_dict_items(self, node: astroid.For) -> None:
187198
# to get the line number where the iterating object was last
188199
# defined and compare that to the for loop's line number
189200
continue
201+
if (
202+
isinstance(subscript.parent, astroid.Assign)
203+
and subscript in subscript.parent.targets
204+
):
205+
# Ignore this subscript if it is the target of an assignment
206+
continue
207+
208+
self.add_message("consider-using-dict-items", node=node)
209+
return
210+
211+
@utils.check_messages("consider-using-dict-items")
212+
def visit_comprehension(self, node: astroid.Comprehension) -> None:
213+
iterating_object_name = _check_if_dict_keys_used(node)
214+
if iterating_object_name is None:
215+
return
216+
217+
children = list(node.parent.get_children())
218+
if node.ifs:
219+
children += node.ifs
220+
for child in children:
221+
for subscript in child.nodes_of_class(astroid.Subscript):
222+
subscript = cast(astroid.Subscript, subscript)
223+
if not isinstance(subscript.value, (astroid.Name, astroid.Attribute)):
224+
continue
225+
226+
value = subscript.slice
227+
if isinstance(value, astroid.Index):
228+
value = value.value
229+
if (
230+
not isinstance(value, astroid.Name)
231+
or value.name != node.target.name
232+
or iterating_object_name != subscript.value.as_string()
233+
):
234+
continue
190235
self.add_message("consider-using-dict-items", node=node)
191236
return

tests/functional/c/consider/consider_using_dict_items.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Emit a message for iteration through dict keys and subscripting dict with key."""
22

3-
# pylint: disable=missing-docstring,unsubscriptable-object
3+
# pylint: disable=missing-docstring,unsubscriptable-object,too-few-public-methods
44

55
def bad():
66
a_dict = {1:1, 2:2, 3:3}
@@ -28,3 +28,36 @@ def another_good():
2828
k = 2
2929
k = 3
3030
print(out_of_scope_dict[k])
31+
32+
33+
b_dict = {}
34+
for k2 in b_dict: # Should not emit warning, key access necessary
35+
b_dict[k2] = 2
36+
37+
for k3 in b_dict: # [consider-using-dict-items]
38+
val = b_dict[k3]
39+
40+
for k4 in b_dict.keys(): # [consider-iterating-dictionary,consider-using-dict-items]
41+
val = b_dict[k4]
42+
43+
class Foo:
44+
c_dict = {}
45+
46+
for k5 in Foo.c_dict: # [consider-using-dict-items]
47+
val = Foo.c_dict[k5]
48+
49+
for k6 in b_dict: # [consider-using-dict-items]
50+
val = b_dict[k6]
51+
b_dict[k6] = 2
52+
53+
c_dict={}
54+
55+
val = [(k7, b_dict[k7]) for k7 in b_dict] # [consider-using-dict-items]
56+
val = any(True for k8 in b_dict if b_dict[k8]) # [consider-using-dict-items]
57+
val = {k9: b_dict[k9] for k9 in b_dict} # [consider-using-dict-items]
58+
val = [(k7, b_dict[k7]) for k7 in Foo.c_dict]
59+
val = any(True for k8 in Foo.c_dict if b_dict[k8])
60+
val = [(k7, c_dict[k7]) for k7 in Foo.c_dict]
61+
val = any(True for k8 in Foo.c_dict if c_dict[k8])
62+
val = [(k7, Foo.c_dict[k7]) for k7 in Foo.c_dict] # [consider-using-dict-items]
63+
val = any(True for k8 in Foo.c_dict if Foo.c_dict[k8]) # [consider-using-dict-items]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
11
consider-using-dict-items:7:4:bad:Consider iterating with .items()
22
consider-using-dict-items:10:4:bad:Consider iterating with .items()
33
consider-using-dict-items:22:4:another_bad:Consider iterating with .items()
4+
consider-using-dict-items:37:0::Consider iterating with .items()
5+
consider-iterating-dictionary:40:10::Consider iterating the dictionary directly instead of calling .keys()
6+
consider-using-dict-items:40:0::Consider iterating with .items()
7+
consider-using-dict-items:46:0::Consider iterating with .items()
8+
consider-using-dict-items:49:0::Consider iterating with .items()
9+
consider-using-dict-items:55:0::Consider iterating with .items()
10+
consider-using-dict-items:56:0::Consider iterating with .items()
11+
consider-using-dict-items:57:0::Consider iterating with .items()
12+
consider-using-dict-items:62:0::Consider iterating with .items()
13+
consider-using-dict-items:63:0::Consider iterating with .items()

0 commit comments

Comments
 (0)