-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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 #4761: Emit used-before-assignment
where single assignment only made in except blocks
#5402
Fix #4761: Emit used-before-assignment
where single assignment only made in except blocks
#5402
Conversation
used-before-assignment
where single assignment only made in except blocks
Pull Request Test Coverage Report for Build 1547071143
π - Coveralls |
6204b7f
to
d5ae954
Compare
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.
Small comment, rest looks good.
However, I think this should be blocked by #5241. We really need that merged first before changing stuff in visit_name
. Otherwise we'll need to keep updating the refactor and we might miss newly added lines.
Hmm yes let me review #5241, I think I messed up with not doing it for 2.12 because this will pretty much change the whole implementation for this once we merge. |
@jacobtylerwalls as someone who used the previous code, your input would be valuable for #5241. If you have the time to review it too I'd appreciate that. |
No problem. I'm having trouble finding the comment, though. Can you post it? |
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.
As said in https://github.com/PyCQA/pylint/pull/5384/files#r757821102 let's do this one before #5384.
Now that the refactor is finally done we can move ahead with this one π
pylint/checkers/variables.py
Outdated
@@ -1221,7 +1251,14 @@ def visit_name(self, node: nodes.Name) -> None: | |||
"undefined-variable", node=node, args=node.name | |||
) | |||
|
|||
current_consumer.mark_as_consumed(node.name, found_nodes) | |||
# Some nodes were filtered out because they execute only on success |
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 don't particularly like how this works, as any new contributor needs to read about this to understand this. It is not as self-explanatory as to_consume
and consumed
. However, I don't know of a better way to do this. Perhaps we could improve on the name? I'm not sure.. I'll think about this some more.
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.
How about consumed_conditionally
?
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.
Either works for me. @Pierre-Sassoulas do you have an opinion?
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.
'maybe_consumed' ? I don't have en opinion either :)
9091bac
to
70fc07f
Compare
β¦nments only take place in except blocks
5b6900e
to
a9e5bd1
Compare
We just refactored `visit_name`
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 these are my last comments. I'll do a final review after your revision.
I wonder if any other contributors can come up with a more self-explanatory name for consumed_uncertain
..
219022f
to
5cd490e
Compare
Co-authored-by: DaniΓ«l van Noord <13665637+DanielNoord@users.noreply.github.com>
This need to be reviewed and merged before #5384 |
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.
Thanks @jacobtylerwalls
Just one final comment! The docstrings and comments really do look good!
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.
Thanks @jacobtylerwalls !
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 nice MR, well documented, well tested, and it fix the try/except logic to something most user would expect imo.
Came across a regression case during testing -> #5500 |
@jacobtylerwalls @DanielNoord Not sure if I should open a ticket, but was testing the latest version of for _ in range(3):
try:
1 / 0
except ZeroDivisionError as e:
error = e # keep error for final raise
else:
break
print("retrying...")
else:
raise error This function works fine, but pylint now complains about a false positive |
Hi @martimlobao, thanks for reporting and for providing a concrete example. I've constructed a couple more examples that I think we'd have to reason about to decide on a way forward. 1. for _ in range(3):
try:
[][0]
except ZeroDivisionError as e:
error = e # keep error for final raise
except:
pass
else:
break
print("retrying...")
else:
raise error When executed, gives: Traceback (most recent call last):
File "a.py", line 12, in <module>
raise error
NameError: name 'error' is not defined 2. for i in range(3):
if 'some runtime condition':
continue
try:
1 / 0
except ZeroDivisionError as e:
error = e # keep error for final raise
else:
break
print("retrying...")
else:
raise error When executed, gives: File "b.py", line 12, in <module>
raise error
NameError: name 'error' is not defined Of course, 1 and 2 are very similar: all that's required is some other means of finishing the inner loop than hitting the except handler you contemplated. These are runtime control flow things that I don't think Pylint is in a position to speculate about. I think it's a virtue that as things stand, you either have to define How does that strike you? |
Thanks for the detailed response @jacobtylerwalls! The issue you're trying to fix here isn't an easy one to solve, even conceptually, so I can definitely appreciate the difficulty in balancing false positives and false negatives. I will say though that from a user's perspective, a false positive error is probably more annoying than a false negative, especially in this case where Pylint classifies this as an actual error and not merely a convention or recommendation. Making a change because of an actual bug is fine, but making it to appease a linter that thinks there's a bug that doesn't exist may feel slightly frustrating. Your examples illustrate well how hard it is to consider every different edge case. I'm not sure how feasible this is as a solution, but would it be possible to check every "exit branch" for a potentially undefined variable? If at least one exit path does not define the variable, then Pylint should raise the error since there's the possibility for a valid Let me try to illustrate what I mean with a few examples: def undefined_variable():
"""used-before-assignment (E0601)"""
try:
pass # < exit branch where some_message is *not* defined
except ValueError:
some_message = 'some message' # < exit branch where some_message is defined
if not some_message:
return 1
return some_message for _ in range(3):
try:
do_something() # < exit branch where error is *not* defined, should flag
except ZeroDivisionError as e:
error = e # < exit branch where error is defined
print("retrying...")
else:
raise error for _ in range(3):
try:
do_something() # not an exit branch because of `else` below (will only exit here with an exception)
except ZeroDivisionError as e:
error = e # < exit branch where error is defined
else:
break # < exit condition where error is *not* defined, but will skip `raise error` below
print("retrying...")
else:
raise error for _ in range(3):
try:
do_something() # not an exit branch because of `else` below (will only exit here with an exception)
except ZeroDivisionError as e:
error = e # < exit branch where error is defined
except:
pass # < exit branch where error is *not* defined, should flag
else:
break # < exit condition where error is *not* defined, but will skip `raise error` below
print("retrying...")
else:
raise error for i in range(3):
if 'some runtime condition':
continue # < exit branch where error is *not* defined, should flag
try: # < other exit branch, needs to check below if error is defined
1 / 0
except ZeroDivisionError as e:
error = e # keep error for final raise
else:
break
print("retrying...")
else:
raise error I have to admit I have no understanding of how feasible it would be to implement this, but I believe this approach should be able to distinguish between (all? most?) scenarios where an unassigned variable exists and those where it does not. |
Thanks for the well-constructed examples. I'm sure in a moment we'll move this discussion to a new issue. Of your last examples, this is the one false positive: for _ in range(3):
try:
do_something() # not an exit branch because of `else` below (will only exit here with an exception)
except ZeroDivisionError as e:
error = e # < exit branch where error is defined
else:
break # < exit condition where error is *not* defined, but will skip `raise error` below
print("retrying...")
else:
raise error But I can easily turn it into a true positive with: for _ in range(3):
try:
do_something() # not an exit branch because of `else` below (will only exit here with an exception)
except ZeroDivisionError as e:
error = e # < exit branch where error is defined
else:
if 'runtime condition':
continue # < exit condition where error is *not* defined, should flag
break # < exit condition where error is *not* defined, but will skip `raise error` below
print("retrying...")
else:
raise error So maybe this is the constraint to check for:
@martimlobao, could you open an issue to track this? I think this specific scenario might be feasible to implement. |
Type of Changes
Description
Before
If an assignment only took place in an except block, later use of the name outside the except did not raise
used-before-assignment
.Now
As long as only one assignment for the name is in the except block, the message is raised.
Closes #4761
Relates to #5384 (we may want to cherry-pick the first commit and rewrite slightly)