-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
New check dict-init-mutate
#7794
Conversation
@nickdrozd I apologize for not being able to retain your commits as they were. I tried lots of things but got enough permission errors when trying to push directly to the original PR that I gave up. |
e20ceb9
to
6bd578f
Compare
Pull Request Test Coverage Report for Build 3533781397
💛 - Coveralls |
eaa26af
to
34c4fa8
Compare
This comment has been minimized.
This comment has been minimized.
Primer output looks really good, in fact this suggestion is just what we would've wanted to suggest. |
77e0b53
to
eb5e2a6
Compare
This comment has been minimized.
This comment has been minimized.
Dict-Init-Mutate checker Messages | ||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | ||
:dict-init-mutate (W3301): *Dictionary mutated immediately after initialization* | ||
Dictionaries can be initialized with a single statementusing dictionary |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dictionaries can be initialized with a single statementusing dictionary | |
Dictionaries can be initialized with a single statement using dictionary |
This looks great! Thanks for picking it up. The primer results are exactly right. I noticed that there is a typo in the PR title ("mudate"). I didn't see it anywhere else, but you should double check just to be sure 😄 |
eb5e2a6
to
c3e3836
Compare
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The change look pretty refined already, let's discuss how we can make the name / message clearer/better.
name = "dict-init-mutate" | ||
msgs = { | ||
"W3301": ( | ||
"Dictionary mutated immediately after initialization", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this message could be more explicit about the solution and the problem, but I'm not sure. We added a function to print big suggestions of dict recently, so maybe it's possible to do something like
"Dictionary mutated immediately after initialization", | |
"Declare all known values directly in the constructor with '%s'", |
Ie. result would be Declare all known values directly in the constructor with ' {"apple": 1, "banana": 10}'
Declare all known values directly in the constructor with ' {"apple": 1, "melon" : 4, ... "banana": 10}'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the updated language, but the suggestion with %s may be tricky because it may be multiple lines after. I'll see what I can do...
class DictInitMutateChecker(BaseChecker): | ||
name = "dict-init-mutate" | ||
msgs = { | ||
"W3301": ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Throughout pylint we have some warning inflation where things that should be Convention
(C) are Warning
(W). This seems like something that could be demoted to convention. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah I agree!
c3e3836
to
8944b53
Compare
def stringify_dict_items(node: nodes.NodeNG) -> str: | ||
try: | ||
key = node.targets[0].slice.value | ||
value = node.value.value | ||
except AttributeError: | ||
# More complex cases will be handled later on | ||
return "" | ||
|
||
if isinstance(value, str): | ||
value = f"'{value}'" | ||
return f"'{key}': {value}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def stringify_dict_items(node: nodes.NodeNG) -> str: | |
try: | |
key = node.targets[0].slice.value | |
value = node.value.value | |
except AttributeError: | |
# More complex cases will be handled later on | |
return "" | |
if isinstance(value, str): | |
value = f"'{value}'" | |
return f"'{key}': {value}" |
We can use node.as_string()
. Check RefactoringChecker._dict_literal_suggestion
and the result in use-dict-literal
functional tests. I think it would be better to move this function to pylint.util
and reuse it, it's pretty polished already. The only thing it could do better is to take care of really long terminating value when the last element is close to 60 chars.
if not isinstance(sibling_name, nodes.Name): | ||
return [node] + all_siblings | ||
|
||
return [node] + self._get_dict_mutating_siblings(sibling, all_siblings) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Much as I love recursion, this will crash Pylint if the dictionary is big enough:
config = {}
config[0] = None
config[1] = None
... # 2 ... 981
config[982] = None
config[983] = None
Hopefully nobody would write something like that by hand! But it could be an issue with generated files.
Maybe it would be best to limit the suggested replacement to checking only a few items (say, three). Then if there are more than that, add an elipsis: Declare all known key/values when initializing the dictionary with 'config = {'dir': 'bin', 'user': 'me', 'workers': 5, ...}'
This could be done in a later PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
very good point, I actually I'm really not loving the additional suggestion. I would vote to simply Declare all known key/values when initializing the dictionary
and not include the constructing dictionary help. It adds difficult to read and maintain code that doesn't add THAT much value. @Pierre-Sassoulas what do you think?
With the example shows in another comment
pylint/extensions/bad_builtin.py:21:0: C3301: Declare all known key/values when initializing the dictionary with 'BUILTIN_HINTS = {'filter': Name.BUILTIN_HINTS(name='BUILTIN_HINTS')}' (dict-init-mutate)
I'm even all for keeping the msg simple.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RefactoringChecker._dict_literal_suggestion
(and the function around it) can probably handle that complexity for us. I'm okay with doing it later on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be awesome. I'll update the PR to leave the general suggestion and open a new issue to add the improvement suggestion.
) | ||
|
||
def _get_dict_mutating_siblings( | ||
self, node: nodes.Assign, all_siblings: List[nodes.Assign] | None = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't use optional-bar syntax in older Python versions 😢
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have python > 3.7.2 we can do it as long as we use from __future__ import annotations
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AHA that's the magic line!!! I couldn't figure it out, thanks Pierre!
Running this checker against Pylint itself yields a funny edge case: BUILTIN_HINTS = {"map": "Using a list comprehension can be clearer."}
BUILTIN_HINTS["filter"] = BUILTIN_HINTS["map"] This gives a confusing error message:
Considering the difficulty in catching these edge cases and how much complexity it adds to the checker, it might be worth keeping the error message simple for now and thinking it over further. Perhaps that could be done in conjunction with figuring out how to catch nested dictionaries. The big challenge is determining what exactly goes in the dictionary. |
70dddf8
to
6c7e0e3
Compare
6c7e0e3
to
3cac8cc
Compare
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks pretty good at this point, the continuous integration needs to be fixed but we can merge otherwise 👍
5c9a34f
to
9fcccfc
Compare
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's possible to rebase now that #7833 is merged.
@@ -0,0 +1,3 @@ | |||
fruits = {} # [dict-init-mutate] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fruits = {} # [dict-init-mutate] | |
fruit_prices = {} # [dict-init-mutate] |
@@ -0,0 +1 @@ | |||
fruits = {"apple": 1, "banana": 10} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fruits = {"apple": 1, "banana": 10} | |
fruit_prices = {"apple": 1, "banana": 10} |
9fcccfc
to
1f2cdd8
Compare
I'm concerned about the warning for https://github.com/pandas-dev/pandas/blob/d1ecf63e2040daecad6e0a8485a35e8a23393795/pandas/tests/reshape/test_get_dummies.py#L110 (item 3 in the Pandas list in this github-actions comment):
(Here, How would one rewrite the snippet? Doing |
Type of Changes
| ✓ | ✨ New feature |
Description
Taking over #5765
Add new check to detect mutating dict after having immediately created it.
Closes #2876