Skip to content

Improved Exception handling#2357

Merged
orbeckst merged 16 commits intoMDAnalysis:developfrom
joaomcteixeira:exceptions_from_e
Nov 5, 2019
Merged

Improved Exception handling#2357
orbeckst merged 16 commits intoMDAnalysis:developfrom
joaomcteixeira:exceptions_from_e

Conversation

@joaomcteixeira
Copy link
Member

I did not rise any issue previously to this PR.

Problem

Exceptions raised while handling previous caught exceptions were not raised from the caught exception nor from None, producing the output:

During handling of the above exception, another exception occurred:

Which in Python means "failed to handle exception properly" or that an uncontrolled exception occurred, which is not the case in MDAnalysis whatsoever.

Solution

By capturing exception as as e and rising exceptions from e the correct output yields:

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

Custom exceptions are raised from None, instead.

I did not entered into the detail of fine tuning from e and from None aside from using from None in custom exceptions because I considered such to be already changing MDAnalysis functionality/interface and I did not want to enter in such detail for this PR.

Thanks to @mariocj89 for his talk at PyCon2019 about correct exception handling.

Tests

Tests run without failure (reds):

platform linux -- Python 3.7.4, pytest-5.1.3, py-1.8.0, pluggy-0.13.0
plugins: hypothesis-4.37.0
15402 passed, 84 skipped, 2 xfailed, 2 xpassed, 31871 warnings in 663.39s (0:11:03)

and pylint,

pylint 2.3.1
astroid 2.2.5
Python 3.7.3 (default, Mar 27 2019, 22:11:17) 
[GCC 7.3.0]

$ pylint --rcfile=package/.pylintrc package/MDAnalysis

------------------------------------
Your code has been rated at 10.00/10

PR Checklist

  • Tests?
  • Docs?
  • CHANGELOG updated?
  • Issue raised/referenced?

@codecov
Copy link

codecov bot commented Sep 29, 2019

Codecov Report

Merging #2357 into develop will decrease coverage by 0.15%.
The diff coverage is 80.89%.

Impacted file tree graph

@@             Coverage Diff             @@
##           develop    #2357      +/-   ##
===========================================
- Coverage    89.92%   89.76%   -0.16%     
===========================================
  Files          175      159      -16     
  Lines        21912    19954    -1958     
  Branches      2874     2875       +1     
===========================================
- Hits         19704    17912    -1792     
+ Misses        1614     1442     -172     
- Partials       594      600       +6
Impacted Files Coverage Δ
package/MDAnalysis/core/__init__.py 79.16% <0%> (ø) ⬆️
package/MDAnalysis/analysis/hole.py 71.96% <0%> (+0.26%) ⬆️
package/MDAnalysis/coordinates/GRO.py 93.33% <100%> (+0.04%) ⬆️
package/MDAnalysis/coordinates/TRJ.py 94.81% <100%> (ø) ⬆️
package/MDAnalysis/coordinates/memory.py 96.05% <100%> (+0.02%) ⬆️
package/MDAnalysis/transformations/fit.py 100% <100%> (ø) ⬆️
package/MDAnalysis/coordinates/MOL2.py 93.44% <100%> (+0.1%) ⬆️
package/MDAnalysis/topology/TOPParser.py 100% <100%> (ø) ⬆️
package/MDAnalysis/transformations/rotate.py 100% <100%> (ø) ⬆️
package/MDAnalysis/analysis/polymer.py 100% <100%> (ø) ⬆️
... and 62 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2501d3e...8ea58e8. Read the comment docs.

@mariocj89
Copy link

This might not work here as the package needs Python2.7 support. You can use six.raise_from though.

@joaomcteixeira
Copy link
Member Author

Hi @mariocj89 , thanks very much for you comment. My fault not realizing the need to support Py2, I always fall into the trap of never considering Py2, may be that's because I actually never code with it.

I've just arrived to the project (as user and as, now, contributor) so I am not yet much deep into the details. Looking into it, I see six is used across the package already, though six.raise_from is never used.

I will wait for the developers to comment further before submitting any other changes.

Best and Thanks!

@orbeckst
Copy link
Member

orbeckst commented Oct 1, 2019

@joaomcteixeira thank you for contributing!

Please use six and make the code Py 2 compatible. We will discontinue Py 2, possibly around the beginning of 2020 but until then it's important that the code runs in 2 and 3.

@orbeckst
Copy link
Member

orbeckst commented Oct 1, 2019

As a first time contributor, please also add yourself to AUTHORS and add an entry to CHANGELOG.

Copy link
Member

@orbeckst orbeckst left a comment

Choose a reason for hiding this comment

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

@joaomcteixeira I haven't reviewed all your changes (that's a lot of work that you've put into this already!). I hadn't had time to watch @mariocj89 's talk. Could you please briefly summarize the rationale for showing all preceding exceptions when we re-raise an exception?

The original idea was that we follow Python's "it is easier to ask for forgiveness than for permission" and sometimes we know (or assume) that an exception as it is occurs is meaningless to the user because the real problem is something different (e.g., when we turn an AttributeError into a TypeError when the wrong input object was provided and our duck-typing failed).

But I am open to hear a different argument. And I am sure that there are a number of cases where your approach is definitely providing additional useful information but before going further I'd like to better understand what we're doing and why.

# treat subselection as AtomGroup
mobile_atoms = subselection.atoms
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.

We prefer variable names with multiple letters such as err as it is a lot easier to search for.

except KeyError:
raise ValueError('Unit ' + str(value) + ' of type ' + str(unit_type) + ' is not recognized.')
except AttributeError:
except KeyError 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.

We prefer variable names with multiple letters such as err as it is a lot easier to search for. (err is also already being used elsewhere, as you saw.)

(This comment applies throughout.)

except AttributeError as e:
raise TypeError("subselection must be a selection string, an"
" AtomGroup or Universe or None")
" AtomGroup or Universe or None") 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.

Do you think the user needs to know why we ended up with the TypeError?

@joaomcteixeira
Copy link
Member Author

Hi @orbeckst
Thanks very much for your reply,
Let me explain this issue further.

Do you think the user needs to know why we ended up with the TypeError?

In the current implementation (master branch), the user will know about both AttributeError and TypeError in the bad way. Which is what happened to me.
Here's a simplification:

try:
    # do something and a AttributeError raises!
    None.atoms()
except AttributeError:
    # We catch the exception
    # and we convert it to a TypeError
    raise TypeError('error to show the user')
    # and this is what the user sees
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-1-0ef4d959c996> in <module>
      2     # do something and a AttributeError raises!
----> 3     None.atoms()
      4 except AttributeError:

AttributeError: 'NoneType' object has no attribute 'atoms'

During handling of the above exception, another exception occurred:

TypeError                                 Traceback (most recent call last)
<ipython-input-1-0ef4d959c996> in <module>
      5     # We catch the exception
      6     # and we convert it to a TypeError
----> 7     raise TypeError('error to show the user')
      8     # and this is what the user sees

TypeError: error to show the user

The key problem here is the sentence:

During handling of the above exception, another exception occurred:

In Python, this strictly means that we were not able to handle the AttributeError properly and inside that except block another Exception occurred that we could not control and were not expecting. The user sees both AttributeError and TypeError where the latter raised uncontrollably. May be a general user won't notice the difference, but a user/developer will and get worried because the message is clear: MDAnalysis crashed, though the error message is user directed.

Solution 1:
Raise TypeError in a controlled manner, simply by raising it from the previous error:

try:
    # do something and a AttributeError raises!
    None.atoms()
except AttributeError as err:
    # We catch the exception
    # and we convert it to a TypeError
    raise TypeError('error to show the user') from err
    # and this is what the user sees
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-2-4cc0bdc2d9f8> in <module>
      2     # do something and a AttributeError raises!
----> 3     None.atoms()
      4 except AttributeError as err:

AttributeError: 'NoneType' object has no attribute 'atoms'

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

TypeError                                 Traceback (most recent call last)
<ipython-input-2-4cc0bdc2d9f8> in <module>
      5     # We catch the exception
      6     # and we convert it to a TypeError
----> 7     raise TypeError('error to show the user') from err
      8     # and this is what the user sees

TypeError: error to show the user

Hence we have:

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

Whatever happened was expected to happen.

Solution 2
Raise TypeError in a controlled fashion hiding AttributeError to the user (notice the from None):

try:
    # do something and a AttributeError raises!
    None.atoms()
except AttributeError:
    # We catch the exception
    # and we convert it to a TypeError
    raise TypeError('error to show the user') from None
    # and this is what the user sees

which outputs only:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-ca5b07d51994> in <module>
      5     # We catch the exception
      6     # and we convert it to a TypeError
----> 7     raise TypeError('error to show the user') from None
      8     # and this is what the user sees

TypeError: error to show the user

Comments
I really think MDAnalysis should raise from err or from None and, decide based on the relevance of that traceback. This is a question that relates to API implementation already. I see from your comment that errors caught should not be presented and only the error raised should; this means that all errors, when raised within except: blocks, should be raised from None. Are these the intentions of the project?

I hope this explanation was helpful.
Looking forward your comments before applying any further changes.
Thanks for all the feedback

@joaomcteixeira
Copy link
Member Author

Hi,
have you considered any further on this matter? Should I actually make it py27 compatible now that it will be over after 2020? Have you decided whether to show the original exception to the user or not?

Look forward to your answer so I can complete the task,
best,

@orbeckst
Copy link
Member

orbeckst commented Nov 2, 2019

@joaomcteixeira thank you for the explanation. You are right that the intention has been

that errors caught should not be presented and only the error raised should

(that's how it worked in Py2, see below)

Make the change – or wait for MDA > 1.0

The Python 3 code should behave in the way the Python 2 code did, as you identified with raise from None.

I don't know if it easy to make this work in Python 2.7: raise ... from None gives SyntaxError in Py2.7. In which version of Python was this featured introduced?

If it is difficult/ugly to make the code work for Py 2 and Py 3 then I would wait for when we retire Python 2 support. The current plan is still somewhat in flux #2303 but we want to do a (more or less) "final" MDAnalysis 1.0 that still supports Python 2.7 with all the features that users know. We then start on 2.0, which will drop Py 2.

(The timeline says "MDA 1.0 by the end of 2019 but this might be a bit ambitious.)

Py2 vs Py3 behavior

I had to check how Py2 behaved because I was surprised to read your explanation. MDAnalysis had started around Python 2.3...

This is how it used to work in Python 2:

Python 2.7.15 | packaged by conda-forge | (default, Jul  2 2019, 00:42:22)
Type "copyright", "credits" or "license" for more information.

IPython 5.8.0 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.

In [1]: foo = 1

In [2]: try:
   ...:     foo.bar
   ...: except AttributeError as err:
   ...:     raise TypeError("Don't use foo")
   ...:
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-2-b01af9547409> in <module>()
      2     foo.bar
      3 except AttributeError as err:
----> 4     raise TypeError("Don't use foo")

TypeError: Don't use foo

but Python 3 change it:

Python 3.6.7 | packaged by conda-forge | (default, Feb 28 2019, 02:16:08)
Type 'copyright', 'credits' or 'license' for more information
IPython 7.5.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: foo = 1

In [2]: try:
   ...:     foo.bar
   ...: except AttributeError as err:
   ...:     raise TypeError("Don't use foo")
   ...:
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-2-2127b0be432b> in <module>
      1 try:
----> 2     foo.bar
      3 except AttributeError as err:

AttributeError: 'int' object has no attribute 'bar'

During handling of the above exception, another exception occurred:

TypeError                                 Traceback (most recent call last)
<ipython-input-2-2127b0be432b> in <module>
      2     foo.bar
      3 except AttributeError as err:
----> 4     raise TypeError("Don't use foo")
      5

TypeError: Don't use foo

updates some missing lines from previous commit
corrects deleted vars
passes tests py37, lookforward towards Travis results
@joaomcteixeira
Copy link
Member Author

Hi @orbeckst ,

Thanks for you comments. I have made the changes accordingly. As suggested above, I used six.raise_from for the Py27 and Py3 compatibility. I maintained the import statements that where already on the original code, that is, where from six import ... was already there I added the raise_from, while where import six was present I used six.raise_from(....) in the code.

The last commit passes all tests for py37 in my computer.

Travis has one broken build 1297.7, says:

************* Module MDAnalysis.units

package/MDAnalysis/units.py:170: [W1618(no-absolute-import), ] import missing `from __future__ import absolute_import`

I think that is nothing introduced in this PR. Can you help me out, is there something I am missing?

Look forward your reply,
Thanks.

@orbeckst
Copy link
Member

orbeckst commented Nov 3, 2019

Can you just add the from __future_ ... to units.py, please? I know that this has nothing to do with your PR. We continuously use updated versions of the CI tools including the linter and it seems a new warning was enabled. It's much easier to ok a PR when all CI comes back green.

Thanks for your massive effort!

Copy link
Member

@orbeckst orbeckst left a comment

Choose a reason for hiding this comment

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

This looks very good, thank you for the effort.

The coverage seems to have decreased, though. I haven't had the time to look where exactly the decrease occurred. It might be that this is simply due to the fact that you increased the number of lines of code insight except statements that were never tested before. In this case, this is ok – it's not the in the scope of this PR to increase test coverage. However, could you please check the coverage report and see if there is code that was tested before but now is not tested anymore?

Then summarize your findings in a comment here. Thank you!

@joaomcteixeira
Copy link
Member Author

Thanks @orbeckst ,
Tests already pass. I will later investigate the coverage issue. It might be because on some calls I assign a string var before raising the exception, so I will try to send the string directly to the raise_from in those cases. I will come back to this later and let you know the outcome.

Coverage might have decreased in previous commits because errmsg
were being defined before raise the errors. Error messages are no longer
defined beforehand and strings are passed directed to Error calls.

I have not tested locally this commit and will rely only on CI online tests.
@orbeckst
Copy link
Member

orbeckst commented Nov 5, 2019

Can you please check why the tests are failing now and fix? (Please excuse my brevity, I’m travelling)

Solves syntax error from 8b0582a
addresses MDAnalysis#2357 (comment)

Tests pass for python3.7 on my computer.
@joaomcteixeira
Copy link
Member Author

joaomcteixeira commented Nov 5, 2019

Hello @orbeckst
The previous commit (8b0582a) failed because of a syntax error; I didn't run the test locally on that commit, sorry.

The latest commits clean the additional variables presented in the first commits of this PR that defined strings that were then passed to the Exceptions call. Now, errors are raised directly without strings being assigned beforehand. That is the only correction I can think of to clear the coverage reduction of this PR.

Yet,

Coverage reports differ from Appveyor and Travis. Actually, when AppVeyor finishes, coverage reports are higher than previously, while these get reduced during Travis, I do not know if these difference in coverage reports is an acknowledged and expected behavior within the project.

By analyzing the coverage reports I cannot understand where more can I operate to increase coverage percentage for the changes I propose apart from actually writing the tests of the cases where Exceptions are expected. Unfortunately, I currently lack the time to dedicate that much to the understanding of MDAnalysis so that I could write those tests properly in a short period of time. I have seen you have been discussion Exception coverage in #597.

Please let me know if there is something more I can help with.

EDIT (after Travis build):

Looking to the Codecov results, I cannot explain properly the sudden decrease in coverage on the last commit and the differences between coverage of the different commits; many of the highlighted code lines are actually not part of this PR. Currently, here ends my knowledge on Codecov reports. I lookforward any discussions.

@orbeckst
Copy link
Member

orbeckst commented Nov 5, 2019

@joaomcteixeira thank you for doing all the extra work. I had a quick look and I don't understand what coverage thinks here either. What you did looks fine to me and I'll squash+merge in a minute.

@orbeckst orbeckst self-assigned this Nov 5, 2019
@orbeckst orbeckst merged commit 7f82b88 into MDAnalysis:develop Nov 5, 2019
@orbeckst
Copy link
Member

orbeckst commented Nov 5, 2019

Congratulations @joaomcteixeira to your first merged commit! Thank you for all your work. I look forward to more contributions!

(I'll send you an invite to the MDAnalysis contributor group.)

@joaomcteixeira
Copy link
Member Author

Hello @orbeckst
Thank you very much for your feedback and for this invitation! I have been using more and more MDAnalysis in my current research; so I am really keen to continue contributing to the project.
Best wishes,

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