-
Notifications
You must be signed in to change notification settings - Fork 152
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
Other event loop scopes? #657
Comments
That's a fair point. I deliberately omitted session scope, but I wasn't aware of package scope. Do you have suggestion how package-scoped (and session-scoped) loops should be defined? |
what's the rationale for omitting session scope? |
There were essentially two ideas for replacing event loop fixture overrides. The first was to have a config option (similar to The other option was to tie the loop scope to a pytest mark. I'm not aware of a way for users to add a mark to the whole session, unless they know about pytest hooks. So in a way, it was an accepted drawback of the current solution. @val-melev Do you have a use case that requires session-scoped loops? If so, can you share it? |
I think if you add package, you probably end up adding session as well: package scope in the top level package looks like session to me, although I don't know if pytest will create/tear down in a slightly different place.
Do you mean behaviour? In which case I think the old (fixture) behaviour is good: the loop gets created when the first test in the package requests it, which happens when you run the first async test, and is destroyed when the last test in the package releases it. If you mean implementation I don't know. I need to look at the new code and its motivation before I can suggest something. As an aside, I wonder if the current behaviour is more breaking than it might appear. It's quite common in the db/backend world to have things like engines or session factories which are bound to an event loop when first accessed and throw if that loop goes away. Whilst you can handle this all in fixtures, it makes sense in integration testing (in my opinion) to let the code do it it's normal way and just override the configuration. I agree that long-lived sessions of any kind don't normally belong in unit testing. |
pytest-asyncio v0.22.0 deprecates overriding the event_loop fixture, and replacing the event_loop fixture with the asyncio_event_loop mark breaks our session scoped asyncio fixtures (e.g., the relay_server fixture). This commit doesn't fix this problem, but at least defers the problems for some time while I figure out how to correctly instrument our shared asyncio fixtures. Related: pytest-dev/pytest-asyncio#657 pytest-dev/pytest-asyncio#658
I was rather wondering how you'd write your test code so it gives you a package or session scoped loop. |
hmm, I can think of a few options, though I'm just brainstorming:
If you have it handy, could we link this issue to the discussion which motivated this change? |
For example, the presence of other async fixtures that have a scope wider than This is why we "extend" the standard scope of the |
Hi, I hope it's helpful to this discussion if I can share my use case for an async fixture which we scope to the session. # conftest.py
@pytest.fixture(scope="session")
async def test_database(database_url: URL) -> URL:
await create_database(database_url)
return database_url We use a fixture like this to create a database to test a web application. We want the database to be created at the point when we start the test session, and reuse that test database for the whole session. This fixture requests the event loop fixture, with session scope. So far, we've been using a fixture like so: # conftest.py
@pytest.fixture(scope="session")
def event_loop() -> Iterator[asyncio.AbstractEventLoop]:
loop = asyncio.new_event_loop()
yield loop
loop.close() From what I've read so far, I don't see a way to make this work with version |
@seifertm (and everyone else) how would you feel about this interface? @pytest.fixture(scope="package", autouse=True)
def custom_event_loop_scope() -> str:
return "package" Or, IMHO clearer: from pytest_asyncio import SCOPE
@pytest.fixture(scope="package", autouse=True)
def custom_event_loop_scope() -> SCOPE:
return SCOPE.package ? This could be inspected in the collection code just like a mark and appropriate action taken. Arguments in favour:
Alternatively the fixture could supply an eventloop policy directly. That might be a fix for #646, since fixture parametrisation would push the problem out of pytest-asyncio's flow? @pytest.fixture(scope="package")
def event_loop_policy() -> AbstractEventLoopPolicy:
return DefaultEventLoopPolicy() This form I think is more powerful, but it would make contradictions less obvious between e.g. a function marked for module scope event loop but requesting a package level event loop policy. We'd still have the scope, so we could just raise at collection. If this is something you'd be interested in I can try to prototype it at the weekend. |
Sharing use case from the other issue We have session scoped async fixtures so we have this central fixture
And every single AsyncGenerator fixtures we have actually require event_loop to make sure the teardown is done before the loop is closed. The main use case for us is we run black box tests using containers we launch part of session scoped fixtures and we return an httpx client configured to talk to the container which we |
Incidentally I've found a few cases in my code where I deliberately requested the event loop fixture in a sync test (either because I needed to interact with a running event loop to test e.g. inspecting/getting loops, but all the methods were sync, or because something deep under the hood would need an event loop but the interface was sync). I don't think it's a problem to deprecate this pattern as you can get exactly the same effect by declaring the test as |
It's become clear that larger loop scopes need to be supported by pytest-asyncio, so this issue is definitely going to be implemented. Thanks for sharing your use cases and API proposals. The current solution with the asyncio_event_loop mark has two problems:
@2e0byo Thanks for establishing precedent for configuration fixtures. It means that users are more likely to be familiar with them. Technically, the event_loop fixture is also a configuration fixture. The problem is that people write all kind of code there. This results in a large number of things that can go wrong which—in turn—makes it difficult to change pytest-asyncio without breaking someone's code. However, I can imagine that fixtures returning a plain string or an enum value, such as in your example, are less problematic.
Looking at my average "lines of code per hour spent" I'd say nothing in pytest-asyncio is easy to implement :) I don't see a way around the split logic in the next release. Users need to be made aware about deprecated functionality (i.e. event loop fixture overrides) and about its replacement functionality. Therefore, both ways must coexist in at least one release. Given the use of pytest-asyncio, I don't see a breaking change without any heads-up information to be an option. Let's try the approach with a dedicated fixture for the loop policy as proposed here. Note that the event loop policy system is going to be deprecated in CPython 3.13 in favour of the loop_factory arguments to asyncio.run and asyncio.Runner. Therefore, I'd prefer we stick to the new naming and call the fixture asyncio_loop_factory, instead. Scoped event loops in v0.22 require the presence of an asyncio_event_loop mark to determine the loop policy. Once the asyncio_loop_factory fixture is in place, we can unconditionally attach an event loop to each pytest collector (Class, Module, Package, Session) and the underlying loop policy is resolved dynamically. Once the loop factory fixture is in place, we can extend the existing asyncio mark with an optional scope kwarg. scope defaults to "function", when missing, to keep the code backwards compatible. Remember that each collector has an event loop. Depending on the chosen scope, each async test can decide in which scope (i.e. in which loop) it runs. The following example runs the two tests in the same even loop: class TestClassScopedLoop:
@pytest.mark.asyncio(scope="class")
async def test_a(self):
...
@pytest.mark.asyncio(scope="class")
async def test_b(self):
... This would also mean that we can keep the standard pytest mark behaviour. The following example is equivalent to the former: @pytest.mark.asyncio(scope="class")
class TestClassScopedLoop:
async def test_a(self):
...
async def test_b(self):
... Trivially, the scope can also be function (the default), module, package, or session. The scope kwarg to the asyncio mark and the asyncio_loop_factory fixture would replace the asyncio_event_loop mark introduced in v0.22. Some playing around with the current code on main suggests this solution is feasible. What's your opinion on this approach? |
On second thought, it would be fine to have a fixture for an event loop policy. We don't have to be faster with deprecating things than upstream. |
Thanks for mentioning that. There are no plans to deprecate the default event_loop fixture for sync tests in the upcoming release. We have to keep in mind that other projects depend on event_loop for compatibility with pytest-asyncio, for example pytest-aiohttp. |
I didn't realise this was coming, but I'm glad (a policy kind of is a factory anyway). On the other hand it looks like a new type will be used, so (ignoring duck typing) it might be better to keep the new name available.
I much prefer it. The kwarg to the asyncio mark is much cleaner IMHO, especially when I'll have a readthrough the WIP and follow that PR. Thanks for being extremely quick to respond to suggestions, and for an excellent library! |
Hi all, and thx for your work. I don't know how helpful this comment will be but I'd like to share the way we use
A session-scoped event loop is crucial for us, as all our services share some heavy-weight async fixtures that must start at the very beginning |
Thanks to everyone who participated in this issue. Sharing your use cases and ideas is the only way pytest-asyncio can make good design decisions and remain useful. pytest-asyncio v0.23.0a0 is available on PyPI and adds an optional scope keyword argument to the asyncio mark to control the scope used for each test. I'd appreciate your feedback on the pre-release version. |
Guys, are working for me. I suffered all day with the problem of inconsistency of asyncpg, sqlalchemy and asyncio. I add myself to requirements.txt pytest-asyncio==0.23.0a0
from app.sql_app.crud import User
from app.sql_app.database import get_conn
from tests.conftest import Conf, mm_user_id
import pytest
@pytest.mark.asyncio(scope="class")
class TestHelp:
endpoint = "/help_info"
async def test_help_info(self, client):
data = {'context': {'acting_user': {'id': mm_user_id, 'username': Conf.test_client_username}}}
response = await client.post(self.endpoint, json=data)
assert response.status_code == 200
@pytest.mark.asyncio(scope="class")
class TestConnections:
endpoint = "/connections"
@pytest.mark.parametrize('login,token,timezone', (
('', '', ''),
("fake_login", 'fake_token', 'UTC'),
("invalid_data", 'invalid_data', 'UTC'),
))
async def test_fail_connect_account(self, client, login, token, timezone):
data = {'context': {'acting_user': {'id': mm_user_id, 'username': Conf.test_client_username}},
# mattermost form data
'values': {'login': login,
'token': token,
'timezone': {'value': timezone},
}
}
response = await client.post(self.endpoint + "/connect_account", json=data)
json_resp = await response.json
assert json_resp['type'] == 'error'
async def test_fail_connect_for_exist_account(self, client):
data = {'context': {'acting_user': {'id': mm_user_id, 'username': Conf.test_client_username}},
# mattermost form data
'values': {'login': Conf.test_client_ya_login,
'token': Conf.test_client_ya_token,
'timezone': {'value': Conf.test_client_ya_timezone},
}
}
async with get_conn() as conn:
# create test_user connection
await User.add_user(
conn,
mm_user_id,
Conf.test_client_ya_login,
Conf.test_client_ya_token,
Conf.test_client_ya_timezone,
e_c=False,
ch_stat=False
)
response = await client.post(self.endpoint + "/connect_account", json=data)
async with get_conn() as conn:
# delete testuser after connection
await User.remove_user(conn, mm_user_id)
json_response = await response.json
assert response.status_code == 200
assert json_response['type'] == 'error'
import asyncio, pytest
from httpx import HTTPError
from app import create_app
import settings
from app.bots.bot_commands import get_user_by_username, create_user
# init testing environments
settings.Conf = settings.Testing
from settings import Conf
# create test user, if it doesn't exist
test_user = asyncio.get_event_loop().run_until_complete(
get_user_by_username(username=Conf.test_client_username))
if isinstance(test_user, HTTPError):
test_user = asyncio.get_event_loop().run_until_complete(create_user({
'email': Conf.test_client_email,
'username': Conf.test_client_username,
'first_name': Conf.test_client_first_name,
'last_name': Conf.test_client_last_name,
'nickname': Conf.test_client_nickname,
'password': Conf.test_client_password,
}))
mm_user_id = test_user['id']
@pytest.fixture # (scope="session")
def app():
app = create_app()
yield app
@pytest.fixture # (scope="session")
def client(app):
return app.test_client()
@pytest.fixture # (scope="session")
def runner(app):
return app.test_cli_runner() |
@ArtemIsmagilov Thanks for testing the alpha release. My understanding is that you're running into problems with pytest-asyncio v0.23.0a0 and that you have provided the code examples for us to look into the problems. Unfortunately, your example is incomplete, so there's no way for me to run it. It also involves setting up configurations and databases that I have no knowledge of. What's more, I don't know the error message you're getting. I'm happy to look into your issue if you can provide a minimal, reproducible example. Otherwise, my hands are tied. |
Thanks for the very quick turnaround on this! I've opened a separate issue with a new bug, but I do appreciate how responsive you've been over this. |
The recent changes deprecated redefining the event_loop fixture, but provided no replacement for
scope=package
. Presumably the move away from the fixture was taken for good reasons, but I'd like to suggest support for (at least) package scope is worth having.Package scope is really handy: you can have a directory structure like this:
with expensive fixtures per-directory, spin them up once per dir and then clean them up (freeing ram) before moving to the next dir.
I know this will work just fine with a session scoped event loop (which is also no longer possible?), but matching the event loop scope to the context has advantages:
Thus I'd like to suggest support for package scope is added to
asyncio_event_loop
. For completeness, the scopes are:I also think it is worth implementing session scoping, although I think
package
is normally the right scope. It's possible I'm missing something here and this is already possible.The text was updated successfully, but these errors were encountered: