Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(consider-interating-dictionary): fix false negatives #5331

Merged
merged 8 commits into from
Nov 30, 2021
5 changes: 5 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ Release date: 2021-11-24

Closes #4412 #5287

* Fix false negative for ``consider-iterating-dictionary`` during membership checks encapsulated in iterables
or ``not in`` checks

Closes #5323

* Fix ``install graphiz`` message which isn't needed for puml output format.

* ``MessageTest`` of the unittest ``testutil`` now requires the ``confidence`` attribute
Expand Down
5 changes: 5 additions & 0 deletions doc/whatsnew/2.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ Extensions
Other Changes
=============

* Fix false negative for ``consider-iterating-dictionary`` during membership checks encapsulated in iterables
or ``not in`` checks

Closes #5323

* Require Python ``3.6.2`` to run pylint.

Closes #5065
9 changes: 5 additions & 4 deletions pylint/checkers/refactoring/recommendation_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,21 +83,22 @@ def _check_consider_iterating_dictionary(self, node: nodes.Call) -> None:
return
if node.func.attrname != "keys":
return
comp_ancestor = utils.get_node_first_ancestor_of_type(node, (nodes.Compare,))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comp_ancestor = utils.get_node_first_ancestor_of_type(node, nodes.Compare)

This could then be simplified.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to #5447 as a follow up issue to this.

I initially wanted to keep the typing of the function simple so thought it would be okay to only allow tuples, but we could allow both.

if (
isinstance(node.parent, (nodes.For, nodes.Comprehension))
or isinstance(node.parent, nodes.Compare)
or comp_ancestor
and any(
op
for op, comparator in node.parent.ops
if op == "in" and comparator is node
for op, comparator in comp_ancestor.ops
if op in {"in", "not in"}
and (comparator in node.node_ancestors() or comparator is node)
DanielNoord marked this conversation as resolved.
Show resolved Hide resolved
)
):
inferred = utils.safe_infer(node.func)
if not isinstance(inferred, astroid.BoundMethod) or not isinstance(
inferred.bound, nodes.Dict
):
return

self.add_message("consider-iterating-dictionary", node=node)

def _check_use_maxsplit_arg(self, node: nodes.Call) -> None:
Expand Down
13 changes: 13 additions & 0 deletions pylint/checkers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
Set,
Tuple,
Type,
TypeVar,
Union,
)

Expand Down Expand Up @@ -280,6 +281,8 @@
)
)

T_Node = TypeVar("T_Node", bound=nodes.NodeNG)


class NoSuchArgumentError(Exception):
pass
Expand Down Expand Up @@ -1690,3 +1693,13 @@ def returns_bool(node: nodes.NodeNG) -> bool:
and isinstance(node.value, nodes.Const)
and node.value.value in {True, False}
)


def get_node_first_ancestor_of_type(
node: nodes.NodeNG, ancestor_type: Tuple[Type[T_Node]]
) -> Optional[T_Node]:
Comment on lines +1698 to +1700
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't know if that has been considered. There is no need to limit ancestor_type to Tuple[...]. isinstance works fine with Type[T_Node] too. Ie.

ancestor_type: Union[Type[T_Node], Tuple[Type[T_Node]]]

This has the advantage that now you can pass both a single type and a tuple of types to it.

"""Return the first parent node that is any of the provided types (or None)"""
for ancestor in node.node_ancestors():
if isinstance(ancestor, ancestor_type):
return ancestor
return None
26 changes: 26 additions & 0 deletions tests/functional/c/consider/consider_iterating_dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,29 @@ def keys(self):
VAR = 1 in {}
VAR = 1 in dict()
VAR = [1, 2] == {}.keys() in {False}

# Additional membership checks
# See: https://github.com/PyCQA/pylint/issues/5323
metadata = {}
if "a" not in list(metadata.keys()): # [consider-iterating-dictionary]
print(1)
if "a" not in metadata.keys(): # [consider-iterating-dictionary]
print(1)
if "a" in list(metadata.keys()): # [consider-iterating-dictionary]
print(1)
if "a" in metadata.keys(): # [consider-iterating-dictionary]
print(1)
yushao2 marked this conversation as resolved.
Show resolved Hide resolved


class AClass:
def a_function(self):
class InnerClass:
def another_function(self):
def inner_function():
another_metadata = {}
print("a" not in list(another_metadata.keys())) # [consider-iterating-dictionary]
print("a" not in another_metadata.keys()) # [consider-iterating-dictionary]
print("a" in list(another_metadata.keys())) # [consider-iterating-dictionary]
print("a" in another_metadata.keys()) # [consider-iterating-dictionary]
return inner_function()
return InnerClass().another_function()
8 changes: 8 additions & 0 deletions tests/functional/c/consider/consider_iterating_dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,11 @@ consider-iterating-dictionary:40:60:40:71::Consider iterating the dictionary dir
consider-iterating-dictionary:43:8:43:21::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED
consider-iterating-dictionary:45:8:45:17::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED
consider-iterating-dictionary:65:11:65:20::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED
consider-iterating-dictionary:73:19:73:34::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED
consider-iterating-dictionary:75:14:75:29::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED
consider-iterating-dictionary:77:15:77:30::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED
consider-iterating-dictionary:79:10:79:25::Consider iterating the dictionary directly instead of calling .keys():UNDEFINED
consider-iterating-dictionary:89:42:89:65:AClass.a_function.InnerClass.another_function.inner_function:Consider iterating the dictionary directly instead of calling .keys():UNDEFINED
consider-iterating-dictionary:90:37:90:60:AClass.a_function.InnerClass.another_function.inner_function:Consider iterating the dictionary directly instead of calling .keys():UNDEFINED
consider-iterating-dictionary:91:38:91:61:AClass.a_function.InnerClass.another_function.inner_function:Consider iterating the dictionary directly instead of calling .keys():UNDEFINED
consider-iterating-dictionary:92:33:92:56:AClass.a_function.InnerClass.another_function.inner_function:Consider iterating the dictionary directly instead of calling .keys():UNDEFINED