Skip to content

Fix exception causes in config/__init__.py #7351

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

Merged
merged 1 commit into from
Jun 14, 2020

Conversation

cool-RR
Copy link
Contributor

@cool-RR cool-RR commented Jun 11, 2020

I recently went over Matplotlib, Pandas and NumPy, fixing a small mistake in the way that Python 3's exception chaining is used. If you're interested, I can do it here too. I've done it on just one file right now.

The mistake is this: In some parts of the code, an exception is being caught and replaced with a more user-friendly error. In these cases the syntax raise new_error from old_error needs to be used.

Python 3's exception chaining means it shows not only the traceback of the current exception, but that of the original exception (and possibly more.) This is regardless of raise from. The usage of raise from tells Python to put a more accurate message between the tracebacks. Instead of this:

During handling of the above exception, another exception occurred:

You'll get this:

The above exception was the direct cause of the following exception:

The first is inaccurate, because it signifies a bug in the exception-handling code itself, which is a separate situation than wrapping an exception.

Let me know what you think!

@nicoddemus
Copy link
Member

I'm 👍 on the idea, thanks for the offer!

Are there still more places in the codebase where this adjustment is needed, or this PR covers them all?

@cool-RR
Copy link
Contributor Author

cool-RR commented Jun 11, 2020

Awesome!

No, there are a few more:

src/_pytest/_code/source.py: 218
src/_pytest/config/__init__.py: 642
src/_pytest/config/__init__.py: 1134
src/_pytest/config/__init__.py: 1201
src/_pytest/config/__init__.py: 1233
src/_pytest/config/argparsing.py: 270
src/_pytest/config/findpaths.py: 29
src/_pytest/debugging.py: 30
src/_pytest/debugging.py: 129
src/_pytest/fixtures.py: 942
src/_pytest/logging.py: 492
src/_pytest/monkeypatch.py: 64
src/_pytest/monkeypatch.py: 73
src/_pytest/python.py: 562
src/_pytest/python.py: 564
src/_pytest/python.py: 583
src/_pytest/python.py: 592
src/_pytest/python.py: 847
src/_pytest/python.py: 1085
src/_pytest/runner.py: 160
src/_pytest/runner.py: 428
src/_pytest/warnings.py: 52
testing/test_config.py: 1647
testing/test_runner.py: 538

Some of these will be false-positives, I'll go over all of them and give you a separate PR after we're done with this PR.

@nicoddemus
Copy link
Member

Thanks!

Wouldn't be better to just change them in this PR instead though?

@cool-RR
Copy link
Contributor Author

cool-RR commented Jun 11, 2020

I could do that, but before I spend time going over these cases, I want to be sure there wouldn't be any roadblock. Getting this PR merged would be a good indication on whether there'll be a roadblock.

@nicoddemus
Copy link
Member

Well the tests passing is a good indicator that things will probably go smooth. 😉

But let's wait for other maintainers to chime in before you spend more time on this. 👍

Copy link
Member

@bluetech bluetech left a comment

Choose a reason for hiding this comment

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

Definitely +1 from me on proper exception chaining.

I've pointed several places where I think from None is more appropriate.

Comment on lines 1133 to 1134
except KeyError as e:
raise ValueError("unknown configuration value: {!r}".format(name)) from e
Copy link
Member

Choose a reason for hiding this comment

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

Here I think from None would be better.

Suggested change
except KeyError as e:
raise ValueError("unknown configuration value: {!r}".format(name)) from e
except KeyError:
raise ValueError("unknown configuration value: {!r}".format(name)) from None

@@ -1223,14 +1223,14 @@ def getoption(self, name: str, default=notset, skip: bool = False):
if val is None and skip:
raise AttributeError(name)
return val
except AttributeError:
except AttributeError as e:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
except AttributeError as e:
except AttributeError:

if default is not notset:
return default
if skip:
import pytest

pytest.skip("no {!r} option found".format(name))
raise ValueError("no option named {!r}".format(name))
raise ValueError("no option named {!r}".format(name)) from e
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
raise ValueError("no option named {!r}".format(name)) from e
raise ValueError("no option named {!r}".format(name)) from None

@@ -641,7 +641,7 @@ def import_plugin(self, modname: str, consider_entry_points: bool = False) -> No
except ImportError as e:
raise ImportError(
'Error importing plugin "{}": {}'.format(modname, str(e.args[0]))
).with_traceback(e.__traceback__)
).with_traceback(e.__traceback__) from e
Copy link
Member

Choose a reason for hiding this comment

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

The with_traceback makes me think maybe the intention here is from None.

@cool-RR
Copy link
Contributor Author

cool-RR commented Jun 11, 2020

My position is that I prefer from old_exception everywhere. I'm okay with skipping a few cases that we don't have agreement on, but I won't add from None, because I don't want to be the reason that someone debugging a problem doesn't have access to potentially helpful information. I'm okay with anyone else adding from None on a separate PR.

If you agree, I'll just remove my changes for all the cases where you've done from None and leave just the ones we agree about.

@bluetech
Copy link
Member

@cool-RR Of course I agree :) I'd be interested to understand your position though.

For example in this case

try:
    description, type, default = self._parser._inidict[name]
except KeyError as e:
    raise ValueError("unknown configuration value: {!r}".format(name)) from e

The KeyError I think would be plain noise, exposing an internal implementation detail. It could have been written just the same as

if name in self._parser._inidict:
    description, type, default = self._parser._inidict[name]
else:
    raise ValueError("unknown configuration value: {!r}".format(name))

@cool-RR
Copy link
Contributor Author

cool-RR commented Jun 11, 2020

@bluetech

I'll happily explain my position :)

I personally hate from None, and I think that when people are debugging they have a very different mindset than when they are writing code. When you're debugging, and you get a very limited amount of information that may have been copy-pasted in an email by a non-technical person, you're very desperate for the information.

In this case, I would have wanted to see the self._parser._inidict[name] traceback. I would maybe get a hint on what happened. If it's a dead end for exploration, I at least want to know its content before I rule it out as a dead end.

Another issue is that some error-reporting systems show not only the traceback, but all the local variables on each of these levels. These supposedly irrelevant levels could have variables that make it clearer what this exception is about.

@bluetech
Copy link
Member

Thanks for explaining your position! So in some cases, like debugging crashes or unexpected exceptions, I fully agree. But in the cases in the PR, the intention is to report some error to the user, and here I subscribe to a different approach, which is to make the errors as concise and to-the-point as possible.

But, I shall not force you to go against your principles 😄 Since a direct cause is already an improvement over an indirect cause (which to me in Python 3 means a programmer error), it's fine by me.

@cool-RR cool-RR force-pushed the 2020-06-11-raise-from branch from 129dbed to caa984c Compare June 12, 2020 15:50
@cool-RR
Copy link
Contributor Author

cool-RR commented Jun 12, 2020

Cool :)

I changed the cases you mentioned, except the one on line 644. I think that the .with_traceback(e.__traceback__), which can actually be removed, means that the traceback of the original exception is relevant for understanding the problem.

@bluetech bluetech merged commit 4f4c263 into pytest-dev:master Jun 14, 2020
@bluetech
Copy link
Member

Thanks @cool-RR!

@cool-RR
Copy link
Contributor Author

cool-RR commented Jun 24, 2020

I just made a blog post about this change here: https://blog.ram.rachum.com/post/621791438475296768/improving-python-exception-chaining-with

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants