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

Implemented new check consider-using-dict-items #4445

Merged
merged 13 commits into from
May 11, 2021

Conversation

yushao2
Copy link
Collaborator

@yushao2 yushao2 commented May 6, 2021

Steps

  • Add yourself to CONTRIBUTORS if you are a new contributor.
  • Add a ChangeLog entry describing what your PR does.
  • If it's a new feature or an important bug fix, add a What's New entry in
    doc/whatsnew/<current release.rst>.
  • Write a good description on what the PR does.

Description

Implemented a new checker, consider-using-dict-items, which checks for instances where a
for loop is iterating over dictionary keys, and the same dictionary is being sub-scripted by the key in the
loop body.

Type of Changes

Type
✨ New feature

Related Issue

Closes #3389

@Pierre-Sassoulas Pierre-Sassoulas added Checkers Related to a checker Enhancement ✨ Improvement to a component labels May 6, 2021
@yushao2
Copy link
Collaborator Author

yushao2 commented May 6, 2021

it seems like one of the pre-commit checks is now failing due to the new check implemented 🤣
I will change the source file to make that compliant

************* Module pylint.checkers.typecheck
pylint/checkers/typecheck.py:1439:8: C0206: Consider iterating with .items() (consider-using-dict-items)

@coveralls
Copy link

coveralls commented May 8, 2021

Coverage Status

Coverage increased (+0.02%) to 91.724% when pulling d9ccab1 on yushao2:checkers-3389 into 547ed55 on PyCQA:master.

Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Choose a reason for hiding this comment

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

Nice new checker. Although I have a feeling the tests are a little light for this kind of checker. I'll check the result on astroid and pygame before approving. (pip3 install git+ssh://git@github.com/yushao2/pylint.git)

@Pierre-Sassoulas Pierre-Sassoulas self-requested a review May 9, 2021 12:35
@yushao2
Copy link
Collaborator Author

yushao2 commented May 9, 2021

Nice new checker. Although I have a feeling the tests are a little light for this kind of checker. I'll check the result on astroid and pygame before approving. (pip3 install git+ssh://git@github.com/yushao2/pylint.git)

I will try to think of more tests for this in the meantime, thanks!

@yushao2
Copy link
Collaborator Author

yushao2 commented May 9, 2021

Nice new checker. Although I have a feeling the tests are a little light for this kind of checker. I'll check the result on astroid and pygame before approving. (pip3 install git+ssh://git@github.com/yushao2/pylint.git)

I found a new bug where the checker failed to detect a new test case

def another_good():
    for k in out_of_scope_dict:
        k = 1
        k = 2
        k = 3
        print(out_of_scope_dict[k])  #should be okay, but error emitted

and my solution to that was to get the subscript's latest assignment line number and compare it to the node (for-loop)'s line number

                last_definition_lineno = value.lookup(value.name)[1][-1].lineno
                if last_definition_lineno > node.lineno:
                    # Ignore this subscript if it has been redefined after
                    # the for loop. This checks for the line number using .lookup()
                    # to get the line number where the iterating object was last 
                    # defined and compare that to the for loop's line number
                    continue

will update my PR with these changes

@cdce8p cdce8p changed the title Implemented new check consider-using-dict-items (#3389) Implemented new check consider-using-dict-items May 9, 2021
Copy link
Member

@cdce8p cdce8p left a comment

Choose a reason for hiding this comment

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

I really like this one! As a test I used it to run pylint over the codebase of home-assistant which resulted in multiple warnings. This MR will definitely improve code quality.

I though, I could contribute some test cases as well. Some of them are not yet passing though.

b_dict = {}
for k2 in b_dict:  # Should not emit warning, key access necessary
    b_dict[k2] = 2

for k3 in b_dict:  # [consider-using-dict-items]
    val = b_dict[k3]

for k4 in b_dict.keys():  # [consider-iterating-dictionary,consider-using-dict-items]
    val = b_dict[k4]

class Foo:
    c_dict = {}

for k5 in Foo.c_dict:  # [consider-using-dict-items]
    val = Foo.c_dict[k5]

for k6 in b_dict:  # [consider-using-dict-items]
    val = b_dict[k6]
    b_dict[k6] = 2
val = [(k7, b_dict[k7]) for k7 in b_dict if b_dict[k7]]  # [consider-using-dict-items]
val = any(True for k8 in b_dict if b_dict[k8])  # [consider-using-dict-items]
val = {k9: b_dict[k9] for k9 in b_dict}  # [consider-using-dict-items]

We could also think about what should happen if an index-lookup is used together with dict.items.

for k10, v in b_dict.items():
    val = v + 2
    print(b_dict[k10])

--
I realize this might all be a bit much at once, but keep in mind: The work you have already done is really good! Especially for a first-time contributor 🐬 These are now just the finishing touches. If you encounter any issues or need help, please don't hesitate to ask.

ChangeLog Outdated Show resolved Hide resolved
doc/whatsnew/2.9.rst Outdated Show resolved Hide resolved
pylint/checkers/refactoring/recommendation_checker.py Outdated Show resolved Hide resolved
pylint/checkers/refactoring/recommendation_checker.py Outdated Show resolved Hide resolved
pylint/checkers/refactoring/recommendation_checker.py Outdated Show resolved Hide resolved
pylint/checkers/refactoring/recommendation_checker.py Outdated Show resolved Hide resolved
pylint/checkers/refactoring/recommendation_checker.py Outdated Show resolved Hide resolved
tests/functional/c/consider/consider_using_dict_items.py Outdated Show resolved Hide resolved
tests/functional/c/consider/consider_using_dict_items.py Outdated Show resolved Hide resolved
@yushao2
Copy link
Collaborator Author

yushao2 commented May 10, 2021

I really like this one! As a test I used it to run pylint over the codebase of home-assistant which resulted in multiple warnings. This MR will definitely improve code quality.

I though, I could contribute some test cases as well. Some of them are not yet passing though.

b_dict = {}
for k2 in b_dict:  # Should not emit warning, key access necessary
    b_dict[k2] = 2

for k3 in b_dict:  # [consider-using-dict-items]
    val = b_dict[k3]

for k4 in b_dict.keys():  # [consider-iterating-dictionary,consider-using-dict-items]
    val = b_dict[k4]

class Foo:
    c_dict = {}

for k5 in Foo.c_dict:  # [consider-using-dict-items]
    val = Foo.c_dict[k5]

for k6 in b_dict:  # [consider-using-dict-items]
    val = b_dict[k6]
    b_dict[k6] = 2
val = [(k7, b_dict[k7]) for k7 in b_dict if b_dict[k7]]  # [consider-using-dict-items]
val = any(True for k8 in b_dict if b_dict[k8])  # [consider-using-dict-items]
val = {k9: b_dict[k9] for k9 in b_dict}  # [consider-using-dict-items]

We could also think about what should happen if an index-lookup is used together with dict.items.

for k10, v in b_dict.items():
    val = v + 2
    print(b_dict[k10])

--
I realize this might all be a bit much at once, but keep in mind: The work you have already done is really good! Especially for a first-time contributor These are now just the finishing touches. If you encounter any issues or need help, please don't hesitate to ask.

Thank you so much for your review! Admittedly I'm still trying to pick up the ropes here so I'm thankful for the review given and for highlighting new test cases/usage scenarios where I haven't thought of. I'll work on the suggested changes first, then try to look at making the new test cases pass.

Thanks once again!

@yushao2
Copy link
Collaborator Author

yushao2 commented May 10, 2021

I really like this one! As a test I used it to run pylint over the codebase of home-assistant which resulted in multiple warnings. This MR will definitely improve code quality.

I though, I could contribute some test cases as well. Some of them are not yet passing though.

b_dict = {}
for k2 in b_dict:  # Should not emit warning, key access necessary
    b_dict[k2] = 2

for k3 in b_dict:  # [consider-using-dict-items]
    val = b_dict[k3]

for k4 in b_dict.keys():  # [consider-iterating-dictionary,consider-using-dict-items]
    val = b_dict[k4]

class Foo:
    c_dict = {}

for k5 in Foo.c_dict:  # [consider-using-dict-items]
    val = Foo.c_dict[k5]

for k6 in b_dict:  # [consider-using-dict-items]
    val = b_dict[k6]
    b_dict[k6] = 2
val = [(k7, b_dict[k7]) for k7 in b_dict if b_dict[k7]]  # [consider-using-dict-items]
val = any(True for k8 in b_dict if b_dict[k8])  # [consider-using-dict-items]
val = {k9: b_dict[k9] for k9 in b_dict}  # [consider-using-dict-items]

We could also think about what should happen if an index-lookup is used together with dict.items.

for k10, v in b_dict.items():
    val = v + 2
    print(b_dict[k10])

--
I realize this might all be a bit much at once, but keep in mind: The work you have already done is really good! Especially for a first-time contributor These are now just the finishing touches. If you encounter any issues or need help, please don't hesitate to ask.

image

image

@yushao2
Copy link
Collaborator Author

yushao2 commented May 10, 2021

We could also think about what should happen if an index-lookup is used together with dict.items.

for k10, v in b_dict.items():
    val = v + 2
    print(b_dict[k10])

--
I realize this might all be a bit much at once, but keep in mind: The work you have already done is really good! Especially for a first-time contributor These are now just the finishing touches. If you encounter any issues or need help, please don't hesitate to ask.

@cdce8p,

I think this should be in a different error message altogether -- should I try my hand at implementing it within this PR or perhaps I should create a separate PR for that. I think it might be better to not bloat up this PR

Copy link
Member

@cdce8p cdce8p left a comment

Choose a reason for hiding this comment

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

A few more comments. This is already looking really good!

As mentioned here, you can move _check_if_dict_keys_used to pylint/checkers/utils.py. I would also recommend renaming it.

A nice to have would be if you could order the individual test cases a bit. The way they are currently, it's sometimes difficult to understand what they actually should test.

--

think this should be in a different error message altogether -- should I try my hand at implementing it within this PR or perhaps I should create a separate PR for that. I think it might be better to not bloat up this PR

I fully agree. If you like, you could take a stab at it after this PR is done.

pylint/checkers/refactoring/recommendation_checker.py Outdated Show resolved Hide resolved
pylint/checkers/refactoring/recommendation_checker.py Outdated Show resolved Hide resolved
tests/functional/c/consider/consider_using_dict_items.py Outdated Show resolved Hide resolved
pylint/checkers/refactoring/recommendation_checker.py Outdated Show resolved Hide resolved
@cdce8p
Copy link
Member

cdce8p commented May 10, 2021

@yushao2 Just one more thing: Could you edit the previous commit messages and remove the issue link and the @ reference? They unfortunately don't work that well together with Github, e.g., I would get a notification anytime the commit is pushed to a new branch.

@yushao2
Copy link
Collaborator Author

yushao2 commented May 11, 2021

A few more comments. This is already looking really good!

As mentioned here, you can move _check_if_dict_keys_used to pylint/checkers/utils.py. I would also recommend renaming it.

A nice to have would be if you could order the individual test cases a bit. The way they are currently, it's sometimes difficult to understand what they actually should test.

--

think this should be in a different error message altogether -- should I try my hand at implementing it within this PR or perhaps I should create a separate PR for that. I think it might be better to not bloat up this PR

I fully agree. If you like, you could take a stab at it after this PR is done.

I just went through the reviews and did the necessary changes -- pardon the amount of commits here, I'll squash the commits in the PR when all looks good and ready to merge

Copy link
Member

@cdce8p cdce8p left a comment

Choose a reason for hiding this comment

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

A few more comments. Mainly about the utils function.

FYI: I pushed a small commit with minor style changes. I though it wasn't worth leaving extra comments for them. Just so you know.

I just went through the reviews and did the necessary changes -- pardon the amount of commits here, I'll squash the commits in the PR when all looks good and ready to merge

You don't need to. I'll squash merge the PR anyway. Just keep what you think is logical or what would improve the review process, i.e., I would appreciate if you don't squash your next changes into the old ones 😄

pylint/checkers/utils.py Outdated Show resolved Hide resolved
pylint/checkers/utils.py Outdated Show resolved Hide resolved
pylint/checkers/utils.py Outdated Show resolved Hide resolved
pylint/checkers/utils.py Outdated Show resolved Hide resolved
@cdce8p
Copy link
Member

cdce8p commented May 11, 2021

Seems like it was necessary after all. py36 - 38 tests are failing. Just interesting that py39 is fine.

if isinstance(value, astroid.Index):
    value = value.value

@yushao2
Copy link
Collaborator Author

yushao2 commented May 11, 2021

Seems like it was necessary after all. py36 - 38 tests are failing. Just interesting that py39 is fine.

if isinstance(value, astroid.Index):
    value = value.value

I'll work on this again in a bit -- got interrupted by some schoolwork matters

@cdce8p
Copy link
Member

cdce8p commented May 11, 2021

I'll work on this again in a bit -- got interrupted by some schoolwork matters

No hurry. Schoolwork is more important

Seems like it was necessary after all. py36 - 38 tests are failing. Just interesting that py39 is fine.

Just looked at the astroid code, turns out astroid.Index is only used until Python 3.8

@yushao2 yushao2 requested a review from cdce8p May 11, 2021 14:10
Comment on lines 1506 to 1521
# Is it a proper keys call?
is_iterating_dict_keys = (
isinstance(node.iter, astroid.Call)
and isinstance(node.iter.func, astroid.Attribute)
and node.iter.func.attrname == "keys"
and isinstance(safe_infer(node.iter.func), astroid.BoundMethod)
)
# Is it a proper dictionary?
is_iterating_dict = isinstance(
node.iter, (astroid.Name, astroid.Attribute)
) and isinstance(safe_infer(node.iter), astroid.Dict)

if is_iterating_dict or is_iterating_dict_keys:
return node.iter.as_string().partition(".keys")[0]

return None
Copy link
Member

Choose a reason for hiding this comment

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

I should have probably explained my suggestion better. IMO this function is now a lot more difficult to understand. What do you think of this one here?

    # Is it a proper keys call?
    if (
        isinstance(node.iter, astroid.Call)
        and isinstance(node.iter.func, astroid.Attribute)
        and node.iter.func.attrname == "keys"
    ):
        inferred = safe_infer(node.iter.func)
        if not isinstance(inferred, astroid.BoundMethod):
            return None
        return node.iter.as_string().rpartition(".keys")[0]

    # Is it a dictionary?
    if isinstance(node.iter, (astroid.Name, astroid.Attribute)):
        inferred = safe_infer(node.iter)
        if not isinstance(inferred, astroid.Dict):
            return None
        return node.iter.as_string()

    return None

Also note the use of rpartition instead of partition.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I understand the usage of rpartition here -- the new test case was written because it was failing on (left) partition 😄

Thank you so much for your continued patience with reviewing my code and giving suggestions -- I'm learning a lot from this!

Let me work on the changes and ping again when I'm done

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Understood your code and what you meant now -- have changed in 797b050

tests/functional/c/consider/consider_using_dict_items.py Outdated Show resolved Hide resolved
pylint/checkers/refactoring/recommendation_checker.py Outdated Show resolved Hide resolved
pylint/checkers/refactoring/recommendation_checker.py Outdated Show resolved Hide resolved
pylint/checkers/refactoring/recommendation_checker.py Outdated Show resolved Hide resolved
@yushao2 yushao2 requested a review from cdce8p May 11, 2021 15:36
Copy link
Member

@cdce8p cdce8p left a comment

Choose a reason for hiding this comment

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

I pushed some last minor style changes and did a final test run. Everything seems to work as expected, so I guess this PR is finished 🚀

Thank you for continuing to work on it, even after those long reviews. You really did an amazing job here. Looking forward to seeing more PRs from you 😉

@cdce8p cdce8p merged commit 6044930 into pylint-dev:master May 11, 2021
@yushao2 yushao2 deleted the checkers-3389 branch May 12, 2021 01:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Checkers Related to a checker Enhancement ✨ Improvement to a component
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Suggest to iterate thorough dict items() instead of keys
4 participants