-
-
Notifications
You must be signed in to change notification settings - Fork 25
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
It's very annoying that fixtures can't yield inside a nursery/cancel scope #55
Comments
So first, there is a way to rewrite our websocket fixture above so that it works. This is ugly, but at least it's a workaround while we're figuring out better options: async def hold_websocket_in_task(*, task_status=trio.TASK_STATUS_IGNORED):
async with open_websocket(...) as websocket:
task_status.started(websocket)
# Do need to be prepared for an exception here... in this example it's fine, but in others it
# might take some care
await trio.sleep(float("inf"))
@pytest.fixture
async def websocket_client(test_nursery):
return await test_nursery.start(hold_websocket_in_task) But surely we can do better than that though. One temptation is to say fine, this wouldn't be a problem if pytest fixtures actually worked like context managers, so maybe trio fixtures should act like context managers. (Well... this wouldn't let us parallelize fixture setup/teardown, but it would at least solve the sequential case.) However, this would break a lot of existing fixture patterns, e.g. this example from the docs: @pytest.fixture
async def rolback_db_connection():
# Setup code
connection = await some_async_db_library.connect(...)
await connection.execute("START TRANSACTION")
# The value of this fixture
yield connection
# Teardown code, executed after the test is done
await connection.execute("ROLLBACK") Also, the way our fixture handling works, you actually need to know a pretty complex set of rules before you can tell which fixtures are Trio fixtures, and which are pytest fixtures. Especially the "if it depends on a trio fixture, it's a trio fixture" rule would be pretty confusing if it also totally changed the semantics of But really the problem isn't exceptions in general... and we don't particularly want to allow fixtures to catch and transform errors from tests; that could get super confusing. The problem is specifically Also, we don't want cancel scopes inside fixtures to be wrapped around other fixtures or the test code, probably.. i.e. if they become cancelled it shouldn't cause other tasks to see (Actually, there's an argument that the current So.... putting all that together..... maybe there is a solution. Looking at the "workaround" above, the trick is that we push the actual fixture setup/teardown off into a separate task, so that it can't enclose anything, and it mostly can't get any exceptions that aren't its fault – the exception being the final What if we make this our general strategy for running fixtures? Every fixture gets its own task, that runs both the setup and teardown code, and we carefully arrange the lifetimes of these tasks so that we trigger the teardown code at the right moment. And... somehow manage the flow of exceptions carefully so that only the right ones go in. I think I need a whiteboard or something to work out the details, and this is already long, so I'm going to hit post. But that's the best idea I've had so far. |
Still working on this. Here are some possibly-incomprehensible notes I scribbled down, so they don't get lost: Setup:
For each fixture:
For the test itself:
After everything has finished: take our list of errors, and raise as MultiError Replace the current |
I just pushed a commit to #50 that implements the strategy described above. It's a huge rework, but I'm pretty happy with how it came out. |
Right now, writing code like this is broken:
The problem is that the cancel scope semantics assume that they're strictly nested and can "see" all exceptions that happen inside them, but the convention for pytest fixtures is that we never throw in any exceptions. Also, right now we setup and teardown fixtures in a strict sequential order, but it might be nice to someday do that concurrently, and if we do then this example would really explode (you'll get assertion errors inside
trio/_core/_run.py
). (Previous discussion: #25 (comment))For this particular case, that's not a big deal – you can write it as
and that's actually better than the original.
But, as @touilleMan points out, there are other cases where this is really quite a problem. Specifically, cases where the use of a nursery is abstracted away inside some other context manager. For example, consider these two fixtures:
The first one is fine; the second one uses a nursery internally, so it's broken. But to realize that, we have to know what's going on internally in some random third-party library, and that's pretty terrible. So... what can we do to make this better?
The text was updated successfully, but these errors were encountered: