Skip to content

bpo-36829: Add test.support.catch_unraisable_exception() - #13490

Merged
vstinner merged 3 commits into
python:masterfrom
vstinner:catch_unraisable_exception
May 22, 2019
Merged

bpo-36829: Add test.support.catch_unraisable_exception()#13490
vstinner merged 3 commits into
python:masterfrom
vstinner:catch_unraisable_exception

Conversation

@vstinner

@vstinnervstinner commented May 22, 2019

Copy link
Copy Markdown
Member
  • Copy test_exceptions.test_unraisable() to
    test_sys.UnraisableHookTest().
  • test_exceptions.test_unraisable() uses catch_unraisable_exception();
    simplify the test. test_sys now checks the exact output.
  • Use catch_unraisable_exception() in test_coroutines,
    test_exceptions, test_generators.

https://bugs.python.org/issue36829

* Copy test_exceptions.test_unraisable() to
test_sys.UnraisableHookTest().
* test_exceptions.test_unraisable() uses catch_unraisable_exception();
simplify the test. test_sys now checks the exact output.
* Use catch_unraisable_exception() in test_coroutines,
test_exceptions, test_generators.
@vstinner

Copy link
Copy Markdown
MemberAuthor

I chose to leave test_io unchanged on purpose: I have a local branch which fix also https://bugs.python.org/issue36918 in Lib/_pyio.py and use this new catch_unraisable_exception() function. Once this PR will be merged, I will write a second PR based on it.


self.assertEqual(repr(cm.unraisable.object), coro_repr)
self.assertEqual(cm.unraisable.exc_type, ZeroDivisionError)
self.assertIn("was never awaited", stream.getvalue())

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

It's a little bit strange to have to catch half of the error message from stderr, and the other half from sys.unraisablehook. My PR #13488 will allow to inject the error message in the unraisable exception to catch both at the same time.

Comment threadLib/test/support/__init__.py Outdated

# check the expected unraisable exception
...
finally:

@graingertgraingertMay 22, 2019

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

this try/with/finally is somewhat tricky, how about:

try:
withsupport.throw_unraisable_exceptions():
...
exceptExceptionase:
... # the exception is now here

eg:

@contextlib.contextmanagerdefthrow_unraisable_exceptions():
unraisable=Noneold_hook=sys.unraisablehookdefhook(exc):
nonlocalunraisableunraisable=excsys.unraisablehook=hooktry:
yieldifunraisableisnotNone:
raiseunraisablefinally:
unraisable=Nonesys.unraisablehook=old_hook

@graingertgraingertMay 22, 2019

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

or recommend usage like this:

withsupport.catch_unraisable_exceptions() ascm:
...
cm.unraisable# only available hereassertcm.unraisableisNone# now it's gone

by updating __exit__:

def__exit__(self, *exc_info):
self.unraisable=None# clear unraisable heresys.unraisablehook=self._old_hook

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good idea :-) I implemented your __exit__ idea to avoid the need for try/finally.

throw_unraisable_exceptions() might be useful, but modified tests needs to access the 'obj' attribute of the unraisable hook. Later, they might also want to get access to the 'err_msg' attribute: #13488

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There's still a sharp edge here:

how about making cm.unraisable throw if accessed outside the context instead of being None, eg:

def __exit__(self, *exc_info):
del self.unraisable
sys.unraisablehook = self._old_hook

@graingertgraingertMay 22, 2019

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

and if you want to access additional unraisablehook info using throw_unraisable_exceptions:

classUnraisableException(Exception):
def__init__(self, unraisable):
self.unraisable=unraisablesuper().__init__(self, unraisable)
@contextlib.contextmanagerdefthrow_unraisable_exceptions():
unraisable=Noneold_hook=sys.unraisablehookdefhook(unraisable_):
nonlocalunraisableunraisable=unraisable_sys.unraisablehook=hooktry:
yieldifunraisableisnotNone:
raiseUnraisableException(unraisable) fromunraisable.exc_valuefinally:
sys.unraisablehook=old_hook

then you can use it with:

try:
withthrow_unraisable_exceptions():
...
exceptUnraisableExceptionase:
print(repr(e.__cause__))
err_msg=e.unraisable.err_msg

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I wrote PR #13554 to implement your "del self.unraisable" idea.

Avoid the need for try/finally: __exit__ clears unraisable to break
the reference cycle.
@vstinner
vstinner merged commit e4d300e into python:masterMay 22, 2019
@vstinner
vstinner deleted the catch_unraisable_exception branch May 22, 2019 21:44
Sign up for freeto 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.

4 participants

@vstinner@graingert@the-knights-who-say-ni@bedevere-bot