-
-
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
Prevent INTERNALERROR in pytest_collectstart when accessing module with pytest.skip #11662
Comments
based on the purpose of the hooks, they are indeed the wrong place to do the work i believe the best way to handle this would be to introduce a way for a node to collect its own factories then both the pytest internal ones as well as pytest-asyncio could add factories in a sensible manner without using the wrong hook |
This sounds like a great idea to me! At least it would solve my original problem. Do you have any thoughts on how the design should look like? Here's my current understanding of your proposal: Side question as I'm not familiar with the naming: Are factories and fixtures synonymous? Or are there factories that aren't fixtures?` [0] see also #6101 which proposes a different approach but a similar goal |
Experiment with a "fixture registration" API: https://github.com/bluetech/pytest/commits/register-fixture/ I converted the internal usages of the "fixture injection hack" to use that instead, which seems to work well. Regarding pytest-asyncioI then tried to convert pytest-asyncio to use See diff
diff --git a/pytest_asyncio/plugin.py b/pytest_asyncio/plugin.py
index eb013f4..87e91f5 100644
--- a/pytest_asyncio/plugin.py
+++ b/pytest_asyncio/plugin.py
@@ -588,10 +588,6 @@ def pytest_collectstart(collector: pytest.Collector):
event_loop_fixture_id = f"{collector.nodeid}::<event_loop>"
collector.stash[_event_loop_fixture_id] = event_loop_fixture_id
- @pytest.fixture(
- scope=collector_scope,
- name=event_loop_fixture_id,
- )
def scoped_event_loop(
*args, # Function needs to accept "cls" when collected by pytest.Class
event_loop_policy,
@@ -604,41 +600,12 @@ def pytest_collectstart(collector: pytest.Collector):
yield loop
loop.close()
- # @pytest.fixture does not register the fixture anywhere, so pytest doesn't
- # know it exists. We work around this by attaching the fixture function to the
- # collected Python object, where it will be picked up by pytest.Class.collect()
- # or pytest.Module.collect(), respectively
- if type(collector) is Module:
- # Accessing Module.obj triggers a module import executing module-level
- # statements. A module-level pytest.skip statement raises the "Skipped"
- # OutcomeException or a Collector.CollectError, if the "allow_module_level"
- # kwargs is missing. These cases are handled correctly when they happen inside
- # Collector.collect(), but this hook runs before the actual collect call.
- # Therefore, we monkey patch Module.collect to add the scoped fixture to the
- # module before it runs the actual collection.
- def _patched_collect():
- collector.obj.__pytest_asyncio_scoped_event_loop = scoped_event_loop
- return collector.__original_collect()
-
- collector.__original_collect = collector.collect
- collector.collect = _patched_collect
- else:
- pyobject = collector.obj
- # If the collected module is a DoctestTextfile, collector.obj is None
- if pyobject is None:
- return
- pyobject.__pytest_asyncio_scoped_event_loop = scoped_event_loop
- # When collector is a package, collector.obj is the package's __init__.py.
- # pytest doesn't seem to collect fixtures in __init__.py.
- # Using parsefactories to collect fixtures in __init__.py their baseid will end
- # with "__init__.py", thus limiting the scope of the fixture to the init module.
- # Therefore, we tell the pluginmanager explicitly to collect the fixtures
- # in the init module, but strip "__init__.py" from the baseid
- # Possibly related to https://github.com/pytest-dev/pytest/issues/4085
- if isinstance(collector, Package):
- fixturemanager = collector.config.pluginmanager.get_plugin("funcmanage")
- package_node_id = _removesuffix(collector.nodeid, "__init__.py")
- fixturemanager.parsefactories(collector.obj, nodeid=package_node_id)
+ collector.session._fixturemanager._register_fixture(
+ name=event_loop_fixture_id,
+ func=scoped_event_loop,
+ scope=collector_scope,
+ nodeid=collector.nodeid,
+ )
def _removesuffix(s: str, suffix: str) -> str: however ran into two issues with cause some package-scoped event loop fixture tests to fail:
|
Reviewed the code, and the |
Great, I'll submit it then. While we still need to work out a public API, this provides a basis for discussion and improves things internally so no reason to hold it. Regarding the Package It's still WIP, I need to handle the backward compat and write a changelog, but it's a nice cleanup even regardless of the bug fix. |
…nodeids Refs pytest-dev#11662. --- Problem Each fixture definition has a "visibility", the `FixtureDef.baseid` attribute. This is nodeid-like string. When a certain `node` requests a certain fixture name, we match node's nodeid against the fixture definitions with this name. The matching currently happens on the *textual* representation of the nodeid - we split `node.nodeid` to its "parent nodeids" and then check if the fixture's `baseid` is in there. While this has worked so far, we really should try to avoid textual manipulation of nodeids as much as possible. It has also caused problem in an odd case of a `Package` in the root directory: the `Package` gets nodeid `.`, while a `Module` in it gets nodeid `test_module.py`. And textually, `.` is not a parent of `test_module.py`. --- Solution Avoid this entirely by just checking the node hierarchy itself. This is made possible by the fact that we now have proper `Directory` nodes (`Dir` or `Package`) for the entire hierarchy. Also do the same for `_getautousenames` which is a similar deal. The `iterparentnodeids` function is no longer used and is removed.
…nodeids Refs pytest-dev#11662. --- Problem Each fixture definition has a "visibility", the `FixtureDef.baseid` attribute. This is nodeid-like string. When a certain `node` requests a certain fixture name, we match node's nodeid against the fixture definitions with this name. The matching currently happens on the *textual* representation of the nodeid - we split `node.nodeid` to its "parent nodeids" and then check if the fixture's `baseid` is in there. While this has worked so far, we really should try to avoid textual manipulation of nodeids as much as possible. It has also caused problem in an odd case of a `Package` in the root directory: the `Package` gets nodeid `.`, while a `Module` in it gets nodeid `test_module.py`. And textually, `.` is not a parent of `test_module.py`. --- Solution Avoid this entirely by just checking the node hierarchy itself. This is made possible by the fact that we now have proper `Directory` nodes (`Dir` or `Package`) for the entire hierarchy. Also do the same for `_getautousenames` which is a similar deal. The `iterparentnodeids` function is no longer used and is removed.
…nodeids Refs pytest-dev#11662. --- Problem Each fixture definition has a "visibility", the `FixtureDef.baseid` attribute. This is nodeid-like string. When a certain `node` requests a certain fixture name, we match node's nodeid against the fixture definitions with this name. The matching currently happens on the *textual* representation of the nodeid - we split `node.nodeid` to its "parent nodeids" and then check if the fixture's `baseid` is in there. While this has worked so far, we really should try to avoid textual manipulation of nodeids as much as possible. It has also caused problem in an odd case of a `Package` in the root directory: the `Package` gets nodeid `.`, while a `Module` in it gets nodeid `test_module.py`. And textually, `.` is not a parent of `test_module.py`. --- Solution Avoid this entirely by just checking the node hierarchy itself. This is made possible by the fact that we now have proper `Directory` nodes (`Dir` or `Package`) for the entire hierarchy. Also do the same for `_getautousenames` which is a similar deal. The `iterparentnodeids` function is no longer used and is removed.
@bluetech Regardless of whether there will eventually be a public API for this functionality, I wanted to thank you for your efforts and for going above and beyond by testing your fixture registration code with pytest-asyncio. |
* feat: The finals implemented * feat: Fix pytest dep until they fix pytest-dev/pytest#11662 * feat: Add test Dockerfile
What's the problem this feature will solve?
Accessing
pytest.Module.obj
triggers a module import. If this happens duringpytest_collectstart
and the module that is about to be collected contains apytest.skip
statement, the result will be an INTERNALERROR. This is either caused by theSkipped
exception or byCollector.CollectError
if theallow_module_level
keyword is missing in thepytest.skip
call.There's no good way to deal with this in the hook, because test outcomes, such as the Skipped exception are not part of the public API.
Describe the solution you'd like
I would like that no INTERNALERROR is triggered when accessing
Module.obj
inpytest_collectstart
Real world use case: Dynamically attaching a fixture to a module, similar to what's done in
Module._inject_setup_module_fixture
:pytest/src/_pytest/python.py
Lines 564 to 577 in 2390610
Alternative Solutions
A workaround is to surround the access to
Module.obj
with a try-except. This forces the import of a non public symbol:https://github.com/pytest-dev/pytest-asyncio/blob/a214c3e77149608d427ccab69140edb509c67697/pytest_asyncio/plugin.py#L592-L619
The text was updated successfully, but these errors were encountered: