-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Slowdown of pytest collection in sqlalchemy in python 3.9+ #9343
Comments
Sounds like #9144. Could you try replacing |
No tests are collected when installing from git, I guess we have some incompatibility with v7 on sqlalchemy side. (no error though) |
Is there a changelog for v7 so I can try seeing what needs changing? The fact that no error is raised makes it hard to pinpoint a particular incompatibility |
Installing from the 6.2.x branch makes no difference in the timing |
Tentatively adding this to the 7.0 project board as a reminder to myself to take a quick look at what might be going on. |
Looked into it, no tests are collected due to #9277. sqlalchemy does some collection magic in its pytestplugin.py which relies on We should either revert #9277, or, if we consider sqlalchemy to be a unique case of this, fix it there. Back to the original issue, @CaselIT you can try the commit before #9277 was merged, i.e.
|
Link to the sqlalchemy pytest plugin: https://github.com/sqlalchemy/sqlalchemy/blob/main/lib/sqlalchemy/testing/plugin/pytestplugin.py (search for |
we can fix the isinstance() call, whats something we can change it towards that will work in pytest 6 and 7 ? (could just be |
btw thanks for the fast help @bluetech ! I was initially a little nervous we wouldn't be able to get quick help with a less obvious issue like this. |
I was only able to take a quick glance at sqlalchemy's plugin code. I can definitely look at it more later, though I can maybe give you a quick answer if you can explain why the plugin checks for (Just to be clear, we will definitely not release something that will break sqlalchemy's plugin -- it only uses public API and our policy is never to break that without at least a deprecation period. The |
BTW, is the plugin only used by sqlalchemy's own tests, or is it meant for public use as well? |
the plugin is used in a public way by other dialects that want to integrate with SQLAlchemy's test suite. So yes, it is public for third party dialects. we are using isinstance() because we iterate through the list of pytest items in pytest_collection_modifyitems and we expand out some of the testing objects into many more tests. As we iterate through this collection, we observed that objects of type "Instance" and these were the objects we actually needed, that is, they represent actual tests. so in this hook we need a way to iterate through items and identify test methods, like "SomeTestSuite().test_foo()" (all of our tests are methods on classes). the other place we use it is in pytest_pycollect_makeitem where we are checking for "Instance" as a means to identify test objects that we want to include in the suite, there seems to be some kind of filtering going on here. as far as what API we need here, it just needs to be that we are able to continue seeing individual tests as objects in some way, and identify them as such. if that's going away altogether then we have bigger issues. |
correction, it looks like "Instance" is how we identify the parent object (the collector?) that owns a series of tests that we want to include or exclude. it's just part of our traversal so we just need to know how to correctly traverse. |
What happened is that we got rid of
To this:
Looking at the plugin I see usages like this: So I think you just need to change similar checks from: if isinstance(item.parent, pytest.Instance) To: if isinstance(item.parent, pytest.Class) You want to keep pytest<7 compatibility, so something like this would be needed at the top of the file: if pytest.version_tuple[0] >= 7:
CollectParent = pytest.Class
else:
CollectParent = pytest.Instance And then: if isinstance(item.parent, CollectParent) Sorry I can't test this myself now. |
Thanks for all the help and suggestions!
I confirm that this commit does collect and also the slowdown in gone:
would it be possible to backport that improvement to the v6 series? @bluetech Regarding your suggestion @nicoddemus that fixes the collection but it will require other changes. The issue is that we use We also tripping an assert while adding a finalizer:
pdbing there in v6 shows that
are are trying to add it in what's the suggested way of adding a class finalizer? |
btw I've started supporting pytest v7 here https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/3332 edit: apart from the edit of edit: no still some work to be done. we seem to be loosing the class variables from the test function that handle the skips. probably related to the parent change |
cant wait for the speedup! ive been pretty depressed about the slow collection times for some time now, there have been different "slow collection" issues over the years (like in the 4.x series too with fixtures). |
We are planning a release candidate pretty soon, likely within the week, so shouldn't be far.
The traceback shows
Ahh I see it in Gerrit. You actually don't need that function, Same is valid for other places like this: items[:] = sorted(
newitems,
key=lambda item: (
get_test_class_from_fn(item).parent.name,
get_test_class_from_fn(item).name,
item.name,
),
) Which can be written as: items[:] = sorted(
newitems,
key=lambda item: (
item.getparent(pytest.Module).name,
item.getparent(pytest.Class).name,
item.name,
),
)
|
yes, that function returns a class in all cases, but an assert was added in v7 that's not present in v6. This is v7 Lines 503 to 511 in e01231c
This is v6 Lines 378 to 383 in cca9b90
there is not check that callitem/node is in the stack, and in both version it's not. Granted sqlalchemy may be doing something that should not do. Basically it's trying to attach a finalizer before the first method of a class, and it's trying to do that in btw, thanks for that |
this is caused by the addfinalizer error, the global class set by sqlalchemy plugin never changes so sqlalchemy continues to checks skips of the class that failed to add finalizers
it no longer seem to be possible to add finalizers before the class and the test has been setup. What I'm observing is that In that sense it's no longer possible adding a finalizer to a class before the class lever fixtures and the first method fixtures are run. I've tries using |
Oh didn't realize that. That has been added by c30feee (cc @bluetech). Hmm yeah that assert assumes However I think we can try something to solve this, today you have this: def pytest_runtest_setup(item):
from sqlalchemy.testing import asyncio
if not isinstance(item, pytest.Function):
return
# pytest_runtest_setup runs *before* pytest fixtures with scope="class".
# plugin_base.start_test_class_outside_fixtures may opt to raise SkipTest
# for the whole class and has to run things that are across all current
# databases, so we run this outside of the pytest fixture system altogether
# and ensure asyncio greenlet if any engines are async
global _current_class
if _current_class is None:
...
def finalize():
global _current_class, _current_report
_current_class = None
...
item.parent.parent.addfinalizer(finalize) The "setup state" machinery is setup during We can debate if we should just go ahead and remove the def pytest_runtest_setup(item):
from sqlalchemy.testing import asyncio
if not isinstance(item, pytest.Function):
return
# pytest_runtest_setup runs *before* pytest fixtures with scope="class".
# plugin_base.start_test_class_outside_fixtures may opt to raise SkipTest
# for the whole class and has to run things that are across all current
# databases, so we run this outside of the pytest fixture system altogether
# and ensure asyncio greenlet if any engines are async
global _current_class
if _current_class is None:
asyncio._maybe_async_provisioning(
plugin_base.start_test_class_outside_fixtures,
item.parent.parent.cls,
)
_current_class = item.parent.parent
def pytest_runtest_teardown(item, nextitem):
# runs inside of pytest function fixture scope
# after test function runs
from sqlalchemy.testing import asyncio
asyncio._maybe_async(plugin_base.after_test, item)
# The code below is the old finalize() in pytest_runtest_setup()
global _current_class, _current_report
if _current_class is None:
return
next_item_is_of_different_class = nextitem is not None and nextitem.getparent(pytest.Class).obj is not _current_class
if next_item_is_of_different_class or nextitem is None:
_current_class = None
try:
asyncio._maybe_async_provisioning(
plugin_base.stop_test_class_outside_fixtures,
item.parent.parent.cls,
)
except Exception as e:
# in case of an exception during teardown attach the original
# error to the exception message, otherwise it will get lost
if _current_report.failed:
if not e.args:
e.args = (
"__Original test failure__:\n"
+ _current_report.longreprtext,
)
elif e.args[-1] and isinstance(e.args[-1], str):
args = list(e.args)
args[-1] += (
"\n__Original test failure__:\n"
+ _current_report.longreprtext
)
e.args = tuple(args)
else:
e.args += (
"__Original test failure__",
_current_report.longreprtext,
)
raise
finally:
_current_report = None It needs some logic to detect that the last test from a class is being torn down (which you get automatically from However I do think we should reconsider just removing the |
Well we posted nearly at the same time hehehe. Let's see, seems convenient to be able to call Let's hear from @bluetech. Meanwhile, I will open a PR removing that assert, then you can install pytest from there to get this problem out of the way. |
many thanks @nicoddemus ! I agree that it may be useful being able to add a finalizer in the before the setup-state has be setup, but I think the solution of using |
Looking at the code it is not so simple, we have refactored that code significantly since then (it is more robust and faster because of that).
That would be great. Could you try it and let us know? |
yes, I noticed that's very different the code from v6 I've tried using will try using your suggestion of splitting the logic |
Ok, I had to use The last version is on gerrit https://gerrit.sqlalchemy.org/c/sqlalchemy/sqlalchemy/+/3332 edit, yes the test all pass. Having the previous behaviour of addfinalizer would be nice to have, but I think this can be closed. Thanks a lot for all the support and help! |
Awesome, that's good news!
Let's see what other maintainers say, but if we keep the assert I think we should add a message to it at least ("addfinalizer called for {item} but setup() not called").
Sure, thanks a lot for the patience! |
I believe you should be able to do away with
I think these are good ideas because it's less reliant on the exact collection hierarchy. The pattern With the above, that leaves one pytest7 check, in the Regarding the I was trying to understand the reason why the hooks are used instead of a class fixture, which are much easier to work with. I see the comment Anyway, thanks for working on a fix on your side, and sorry for the breakage. I hope you can at least see why we're getting rid of |
What the SQLAlchemy plugin is trying to do before with in As mentioned above if there is a way of adding a finalizer before the class is setup it may make the code a bit easier to follow in the SQLAlchemy plugin, but the current solution is also ok!
No problem, SQLAlchemy does some unusual stuff for sure in the tests, so we initially though it would be a lot more complex to make v7 work I'll close this, since v7 is fixing the issue! Thanks again for the support |
I opened #9347 to track the remaining issue about you not getting a warning when accessing |
thanks, I've applied your suggestions. There is now no need of using any pytest7 check |
pip list
from the virtual environment you are usingWe noticed that from python 3.9 pytest takes a significantly more time (~4x) to collect the tests for sqlalchemy.
You can run the following docker to compare. python 3.7 and 3.8 are ok (not fast not slow), python 3.9 and 3.10 are noticeably slower
the times on my pc are as floows:
cc @zzzeek
The text was updated successfully, but these errors were encountered: