-
Notifications
You must be signed in to change notification settings - Fork 179
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
Warn against reusing exception names after the except: block on Python 3 #59
Conversation
|
||
Last line will raise UnboundLocalError on both Python 2 and | ||
Python 3 because the existence of that exception name creates a local | ||
scope placeholder for it, obscuring any globals, etc.""" |
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.
Wow, that is sneaky. Good catch.
Looks pretty good, thanks for the patch. There's one more case to consider which might need to be addressed: # test.py
def f():
e = 'foo'
try:
pass
except Exception as e:
pass
print(e)
f() $ python3 test.py
foo
$ pyflakes test.py
test.py:8: undefined name 'e' There's a similar difference in the scope of bound names between Python 2 and 3 with generator expressions. You might look at |
@bitglue, this case is handled correctly as is. The code in your example would fail if the exception is raised. You're only including exception handling because, well, sometimes those exceptions will be raised. You don't want to be surprised exactly in this scenario. Even if the code executes just fine without the exception, PyFlakes should warn that in the case of an exception there's going to be trouble. |
What about the slightly more complicated: def maybe_raise_exception():
import random
if random.randint(0, 1):
raise Exception('whoops')
def f():
e = 'foo'
try:
maybe_raise_exception()
except Exception as e:
print(e)
raise SystemExit(1)
print(e)
f() |
Haha, sure, if we're re-raising or otherwise exiting the flow of the function in the Instead of going that path, let's agree that your last example is still a code smell in the sense that there's no need for the exception name to shadow an unrelated local (or global) name. So, how about I implement warning against reusing the exception name if it already exists in any scope above? It would be in the spirit of "function redefinition". |
# the exception, which is not a Name node, but a simple string. | ||
if isinstance(node.name, str): | ||
self.handleNodeStore(node) | ||
if not isinstance(node.name, str): |
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.
please use if PY2:
instead of type inspection, so that it is more clear that this is version specific code
This should only generate an error on Python 3. It is valid to refer to the exception variable in Python 2 |
Reusing names as an exception variable name is valid, and is quite common based on my analysis done for #57. Reusing a function name is different; the previous definition is replaced and becomes inaccessible, which isnt appropriate for a public symbol. Very likely the code should have better usage of module level Anyways, you don't need complex heuristics to avoid the problematic case that @bitglue has pointed out. There are two solutions:
|
|
Re 2, the comparison is wrong. the exception names are local; if the code works, naming of variables is a style issue and doesnt affect anyone else. function names are public. There are good reasons to re-use existing names for exception names. Existing very large code-bases do it. The code @bitglue has given above is effectively what is really done. Is it nice? no. But it is common enough that we cant say it is illegal. Wrt my suggestions, I didnt go into every niggly implementation detail in order that they could be understood at a high level. |
Remember that we're talking about Python 3 code here. "if the code works" - the entire point is that on Python 3 it doesn't because the behavior is different depending on whether the exception was raised or not. That's not style, that's correctness. Warning the programmer that he's redeclaring the name would help finding bugs. If there are reasons to bind exception names to locals, this should be done by explicit assignment, like the example in |
Not quite, I think. An example of function redefinition, with pyflakes will complain about: def f():
print("foo")
def f():
print("bar") This program, no matter how you run it, can never print "foo". This is not just a smell: there is no reason at all to want the first definition of Change the program just a little bit: def f():
print("foo")
f()
def f():
print("bar") Now there is a reason for the first Would it be possible to relax the constraints somewhat so there may be false negatives, but there are no false positives? I think this would be more in line with the design principles. |
To be clear, on both Python 2 and Python 3, the code sample that @bitglue mentioned here and reproduced below will absolutely replace that local value. In Python 3, that means that if there's already a local with that name then this warning should be skipped. (I haven't reviewed the code or tested this PR out yet though so that may already be the case.) def maybe_raise_exception():
import random
if random.randint(0, 1):
raise Exception('whoops')
def f():
e = 'foo'
try:
maybe_raise_exception()
except Exception as e:
print(e)
raise SystemExit(1)
print(e)
f() In other words in |
Thanks for taking the time to review this! @sigmavirus24, the behavior is different on Python 3 from what you describe. Let's separate two parts of the issue here. First, let's start with the full exception situation and then discuss false positives, "styling suggestions", etc. Availability of exception names after the
|
I disagree with your first finding "Code that involves exception handling but only behaves as expected when the exception is not raised, is incorrect." as written. Consider the following:
In the above, It is not trivial to determine whether the code from the The same applies to "Code that involves exception handling but only behaves as expected when the exception is raised, is incorrect." as written. As before, to avoid false positives you would need to track breakages of execution in the Consider the following for a rather sensible bit of code, where reusing the exception name is desirable:
Note that your global variable examples (3 and 6) are not failing for the reason you believe they are failing. As Also note that it should not be prohibitively complex to behave differently if a variable is global; the variables that are from the global scope are easy to identify within pyflakes. |
I do agree that handling local name shadowing while supporting flow control breaks would be a nice feature. But saying "it is not trivial to determine whether the code from the except: block flows to the block that follows, but it also isn't terribly complex" just goes to show that you didn't think it through, @jayvdb . What if the re-raise (https://gist.github.com/ambv/5925849ac299cd15386d, but really any exception applies) happens within a function that you call in the So, we're back in square one where, as I commented before, I'm proposing to do what @sigmavirus24 suggested, which is to silence the "undefined name" warnings for local name shadowing. I would do the same for global name shadowing since it has the same false positive problem. But in this case I don't have to because that warning didn't work for global name shadowing in the first place. In PyFlakes, either something's bound in the current scope or it doesn't exist (and then we're looking at nonlocal scopes to find it). Unbound local discovery in PyFlakes is done by checking assignments against names from outer scopes "used" before within the current scope. In our problem the unbound read happens after the implicit assignment by the And to close this, please read line 73 in |
Sure, an exception occurring within an And my apologies for assuming that you didnt understand that In your proposed solution, would there be an error on |
Keep in mind that pyflakes doesn't have warnings. It only has errors. It is important to me, and I suspect many other people who put pyflakes in their automated testing process, that it continues to not emit errors for any code which does not have actual errors. If I'm understanding correctly, you are proposing this change as a solution for example5. Here it is, for discussion: def example5():
e5 = 'old local value'
try:
raise ValueError('ve5')
except Exception as e5:
pass
print(e5)
# py2: prints 've5'
# py3: raises UnboundLocalError I think in this case we can only be sure there's an error on Python 3 because we can be sure the However, many times there will be something other than Secondly, consider this: def example5b():
e5 = 'old local value'
failed = False
try:
raise ValueError('ve5')
except Exception as e5:
failed = True
if not failed:
print(e5) This prints nothing but also does not have any undefined names in Python 2 or 3. Pyflakes does not currently have any concept of conditional branches, so it treats this code as if the I'm reluctant to concede that because this case is complicated, pyflakes should just emit an error because it might be a problem and the author might have made a mistake. This just isn't in the design principles, and it's a big part of what differentiates pyflakes from pylint.
My personal belief is this sort of thing belongs in unit tests. Pyflakes is there to catch problems that a unit test won't, like unused imports, or dead code. It just happens to catch some other errors, like undefined names, because it's easy for pyflakes to do so. I've changed my mind on issues like this before. The single most persuasive argument you can make is to run your proposed change against a lot of existing Python code and demonstrate that there are no false positives. |
@bitglue, okay, what you said makes perfect sense to me. I'm modifying the pull request to only issue errors on 100% clear cut cases (e.g. Python 3 with no shadowing at all so no controversy and false positives). PyFlakes doesn't have warnings so my suggestion to introduce the "redefinition check" is not applicable. Fair enough, I agree. @sigmavirus24, @bitglue, where would this redefinition warning fit? Would |
pep8 probably not, since I don't think there's anything in PEP 8 about it. Personally, I'll catch these errors in unit tests. You could also check out pylint. |
fa8861e
to
3d1f8de
Compare
As discussed, I updated the pull request to only issue warnings if no shadowing is happening at all. I left all discussed false positives as tests to make sure they never issue warnings if we do end up extending this functionality in the future. I decided to write a custom flake8 plugin to handle warning against exception name redefining a local/nonlocal. This way projects can enable this should they choose to. |
@bitglue, is there anything else here that you'd like me to do before merging this? |
Looks good -- can you please rebase onto master so I can fast-forward merge it? |
Done! |
as expected, this is causing false positive errors on python 3 |
(ughh.. ignore me going off half cocked, it is finding lots of real errors as far as I can see.) |
If you see any false positives, give us examples here. I cut the pull request's scope by a lot to ensure there's no false positives (see my last message on March 14). If there's still any, we should fix it. |
Found an exception https://bugs.launchpad.net/pyflakes/+bug/1578903 |
The following code is invalid on Python 3:
This pull request makes the Checker warn against this problem. There are new tests that pass on both Python 2 and Python 3.