-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
unraisablehook enhancements #12958
base: main
Are you sure you want to change the base?
unraisablehook enhancements #12958
Conversation
# test_demo.py
import asyncio
import pytest
async def my_task():
pass
def test_scheduler_must_be_created_within_running_loop() -> None:
with pytest.raises(RuntimeError) as _:
asyncio.create_task(my_task()) This is an improvement it works now if you do: |
These fixes probably need to be applied to the thread hooks |
try: | ||
import tracemalloc | ||
except ImportError: | ||
return "" |
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.
this should be covered on pypy
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.
Leave that comment in the code? More likely for future maintainers to see it that way 🙂
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.
Ah what I mean to say is that the coverage check is failing for this branch, but it shouldn't and I don't know why
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.
Looks great overall! Some comments below, merge when you think it's ready.
try: | ||
import tracemalloc | ||
except ImportError: | ||
return "" |
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.
Leave that comment in the code? More likely for future maintainers to see it that way 🙂
# TODO: should be a test failure or error | ||
assert result.ret == pytest.ExitCode.INTERNAL_ERROR |
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.
Do we want to change this now?
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.
Yes, but I don't know how
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.
I haven't looked at the code at all, but maybe set a flag from the unraisable hook, check it where we choose the exit code, and exit-with-error if it's set?
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.
Thanks, these are nice improvements.
I left some comments.
|
||
_unraisable_exceptions: collections.deque[UnraisableMeta | BaseException] = ( |
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.
Can we use the config stash for this instead of a global?
Of course, unraisablehook
itself is a global, so it's not like we can really make this plugin safe for multiple concurrent pytest sessions. But still, I prefer to avoid globals when possible.
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.
How do I get to the config from the various test stage hooks?
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.
They can all receive an pytest.Item
, so you can access the config via item.config
.
while True: | ||
try: | ||
meta = _unraisable_exceptions.pop() | ||
except IndexError: | ||
break |
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.
Perhaps like this? It's shorter and makes the stop condition more obvious.
while True: | |
try: | |
meta = _unraisable_exceptions.pop() | |
except IndexError: | |
break | |
while _unraisable_exceptions: | |
meta = _unraisable_exceptions.pop() |
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.
The current code only checks if the deque has an item once, and while some_deque: some_deque.pop()
is an anti pattern because it's not thread safe (we don't care about it here, but we might as well use the thread safe version)
src/_pytest/unraisableexception.py
Outdated
) | ||
) | ||
except BaseException as e: | ||
_unraisable_exceptions.append(e) |
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.
Can we make this an UnraisableMeta
as well, perhaps preserving the original unraisable just without the extra info?
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.
this BaseException handling needs to be as simple as possible - the failures here are the sorts of thing relating to missing globals and things. I've refactored to make this branch only load locals
|
||
class UnraisableMeta(NamedTuple): |
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.
I'd use a dataclass; named tuples feel a bit outdated these days. But no problem if you want to keep it.
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.
I have a gut feeling that NamedTuple is better here
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.
Can you elaborate? It is only being used for attribute access, never as an actual tuple.
I agree with @bluetech, in general I go with dataclass
, only reaching for a NamedTuple
when I'm updating an existing API that used to return a tuple
. For new APIs, I prefer using a dataclass
as NamedTuples have some problems:
https://codereview.doctor/features/python/best-practice/avoid-named-tuple-use-dataclass
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.
In the end though, is not a big problem if you really want to use a NamedTuple
for some reason, given it is only internal and not public API.
Co-authored-by: Ran Benita <[email protected]>
I just noticed that raising an exception in a callback to Config.add_cleanup doesn't allow subsequent cleanups to run. I'll fix this in another pr |
the sorts of errors possible include _unraisable_exceptions being removed from the unraisablehook plugin module dict, so we bind append as a local
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.
Overall great work, left some minor comments.
|
||
class UnraisableMeta(NamedTuple): |
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.
In the end though, is not a big problem if you really want to use a NamedTuple
for some reason, given it is only internal and not public API.
del errors, meta, hook_error | ||
|
||
|
||
def _cleanup(*, prev_hook: Callable[[sys.UnraisableHookArgs], object]) -> None: |
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.
Doesn't matter much, asking more for consistency, but any reason why we are using a leading _
here? I think otherwise all functions in this module are not private API anyway.
|
||
_unraisable_exceptions: collections.deque[UnraisableMeta | BaseException] = ( |
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.
They can all receive an pytest.Item
, so you can access the config via item.config
.
Co-authored-by: Bruno Oliveira <[email protected]>
Co-authored-by: Bruno Oliveira <[email protected]>
Co-authored-by: Bruno Oliveira <[email protected]>
Co-authored-by: Bruno Oliveira <[email protected]>
A number of unraisablehook plugin enhancements:
A lot of these enhancements could also apply to the threading.excepthook plugin and I'll try to do that in a followup
Could help with #10404 it doesn't fix the issue yet because the warnings filter is removed before the session finishes, so any warnings that occur during session/config teardown are not reported. This PR improves the situation because it's now possible to configure warnings as errors by setting the PYTHONWARNINGS environment variable and ensure all unraisable exceptions are collected
It may also be a good idea to add a flag that calls gc.collect() at the end of every test phase to help associate errors with tests - but probably you don't want to run this every time as it impacts performance