Skip to content
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

ExitStack.__exit__ incorrectly suppresses exceptions in __exit__ callbacks of inner context managers #63291

Closed
hniksic mannequin opened this issue Sep 25, 2013 · 10 comments
Assignees
Labels
stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error

Comments

@hniksic
Copy link
Mannequin

hniksic mannequin commented Sep 25, 2013

BPO 19092
Nosy @warsaw, @ncoghlan, @orsenthil, @hniksic
Files
  • exitstack.diff: patch described in the initial report
  • exitstack.diff: Updated patch
  • exitstack.diff: Updated patch, with fixed context
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = 'https://github.com/ncoghlan'
    closed_at = <Date 2013-10-01.13:28:18.197>
    created_at = <Date 2013-09-25.19:09:47.345>
    labels = ['type-bug', 'library']
    title = 'ExitStack.__exit__ incorrectly suppresses exceptions in __exit__ callbacks of inner context managers'
    updated_at = <Date 2014-01-12.15:02:48.450>
    user = 'https://github.com/hniksic'

    bugs.python.org fields:

    activity = <Date 2014-01-12.15:02:48.450>
    actor = 'orsenthil'
    assignee = 'ncoghlan'
    closed = True
    closed_date = <Date 2013-10-01.13:28:18.197>
    closer = 'python-dev'
    components = ['Library (Lib)']
    creation = <Date 2013-09-25.19:09:47.345>
    creator = 'hniksic'
    dependencies = []
    files = ['31872', '31897', '31908']
    hgrepos = []
    issue_num = 19092
    keywords = ['patch']
    message_count = 10.0
    messages = ['198411', '198421', '198427', '198428', '198548', '198559', '198623', '198773', '207926', '207959']
    nosy_count = 5.0
    nosy_names = ['barry', 'ncoghlan', 'orsenthil', 'hniksic', 'python-dev']
    pr_nums = []
    priority = 'normal'
    resolution = 'fixed'
    stage = 'resolved'
    status = 'closed'
    superseder = None
    type = 'behavior'
    url = 'https://bugs.python.org/issue19092'
    versions = ['Python 3.3', 'Python 3.4']

    @hniksic
    Copy link
    Mannequin Author

    hniksic mannequin commented Sep 25, 2013

    While using contextlib.ExitStack in our project, we noticed that its __exit__ method of contextlib.ExitStack suppresses the exception raised in any contextmanager's __exit__ except the outermost one. Here is a test case to reproduce the problem:

    class Err:
      def __enter__(self): pass
      def __exit__(self, *exc): 1/0
    
    class Ok:
      def __enter__(self): pass
      def __exit__(self, *exc): pass
    
    
    import contextlib
    s = contextlib.ExitStack()
    s.push(Ok())
    s.push(Err())
    with s:
      pass

    Since the inner context manager raises in __exit__ and neither context manager requests suppression, I would expect to see a ZeroDivisionError raised. What actually happens is that the exception is suppressed. This behavior caused us quite a few headaches before we figured out why we the exceptions raised in during __exit__ went silently undetected in production.

    The problem is in ExitStack.__exit__, which explicitly propagates the exception only if it occurs in the outermost exit callback. The idea behind it appears to be to simply record the raised exception in order to allow exit callbacks of the outer context managers to see the it -- and get a chance to suppress it. The only exception that is directly re-raised is the one occurring in the outermost exit callback, because it certainly cannot be seen nor suppressed by anyone else.

    But this reasoning is flawed because if an exception happens in an inner cm and none of the outer cm's chooses to suppress it, then there will be no one left to raise it. Simply returning True from ExitStack.__exit__ is of no help, as that only has an effect when an exception was passed into the function in the first place, and even then, the caller can only re-raise the earlier exception, not the exception that occurred in the exit callback. And if no exception was sent to ExitStack.__exit__, as is the case in the above code, then no exception will be re-raised at all, effectively suppressing it.

    I believe the correct way to handle this is by keeping track of whether an exception actually occurred in one of the _exit_callbacks. As before, the exception is forwarded to next cm's exit callbacks, but if none of them suppresses it, then the exception is re-raised instead of returning from the function. I am attaching a patch to contextlib.py that implements this change.

    The patch also makes sure that True is returned from ExitStack.__exit__ only if an exception was actually passed into it.

    @hniksic hniksic mannequin added stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error labels Sep 25, 2013
    @ncoghlan
    Copy link
    Contributor

    Yep, as indicated by the patch, looks like just a bug with the location of the raise in the stack emulation.

    The contextlib tests will also need a new test case to cover this, as well as one to cover such an exception being suppressed by an outer manager.

    @hniksic
    Copy link
    Mannequin Author

    hniksic mannequin commented Sep 26, 2013

    Nick, thanks for the review. Do you need me to write the patch for the test suite along with the original patch?

    @ncoghlan
    Copy link
    Contributor

    That would be very helpful!

    @hniksic
    Copy link
    Mannequin Author

    hniksic mannequin commented Sep 28, 2013

    Here is the updated patch, with a very minor improvement (no longer unnecessarily holds on to original exc_info), and with new tests. The tests test for the non-suppression of exit-exception (which fails without the fix) and for the correct suppression of body-exception by an outer CM. It was not necessary to write a test for suppression of exit-exception, since this is already tested by test_exit_exception_chaining_suppress().

    There is one problem, however: applying my patch mysteriously breaks the existing test_exit_exception_chaining(). It breaks in a peculiar way: the correct exception is propagated , but the exception's context is wrong. Instead of to KeyError, it points to ZeroDivisionError, despite its having been correctly suppressed.

    I placed prints in _fix_exception_context right before assignment to __context__, to make sure it wasn't broken by my patch, and the assignment appears correct, it sets the context of IndexError to KeyError instance. The __context__ is correct immediately before the raise statement. However, after the IndexError is caught inside test_exit_exception_chaning(), its __context__ is unexpectedly pointing to ZeroDivisionError.

    It would seem that the difference is in the raise syntax. The old code used "raise", while the new code uses "raise exc[1]", which I assume changes the exception's __context__. Changing "raise exc[1]" to "raise exc[1] from None" didn't help.

    @ncoghlan
    Copy link
    Contributor

    Moving the context fixing into an exception handler may work. Something
    like:

    try:
        raise exc[1]
     except BaseException as fix_exc:
        ...
        raise
    

    @hniksic
    Copy link
    Mannequin Author

    hniksic mannequin commented Sep 29, 2013

    Indeed, that works, thanks. Here is the updated patch for review, passing all tests.

    @ncoghlan ncoghlan self-assigned this Oct 1, 2013
    @python-dev
    Copy link
    Mannequin

    python-dev mannequin commented Oct 1, 2013

    New changeset 423736775f6b by Nick Coghlan in branch '3.3':
    Close bpo-19092: ExitStack now reraises exceptions from __exit__
    http://hg.python.org/cpython/rev/423736775f6b

    New changeset 451f5f6151f5 by Nick Coghlan in branch 'default':
    Merge bpo-19092 from 3.3
    http://hg.python.org/cpython/rev/451f5f6151f5

    @python-dev python-dev mannequin closed this as completed Oct 1, 2013
    @python-dev
    Copy link
    Mannequin

    python-dev mannequin commented Jan 12, 2014

    New changeset a3e49868cfd0 by Senthil Kumaran in branch '3.3':
    Issue bpo-19092 - Raise a correct exception when cgi.FieldStorage is given an
    http://hg.python.org/cpython/rev/a3e49868cfd0

    New changeset 1638360eea41 by Senthil Kumaran in branch 'default':
    merge from 3.3
    http://hg.python.org/cpython/rev/1638360eea41

    @orsenthil
    Copy link
    Member

    The last tracker message msg207926 is applicable to issue bpo-19097 and not here. Sorry for the confusion.

    @ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error
    Projects
    None yet
    Development

    No branches or pull requests

    2 participants