Background
Pytest stores fixture definitions (FixtureDefs) for the same name in a list. When a test requests a fixture (by name), the fixturedef to use is picked as follows:
- The fixturedefs that are not visible to the node are filtered out (matchfactories)
- Then the last remaining fixturedef in the list (i.e. at position -1) is used
If the used fixturedef itself then requests the same fixture name, then the next one in the list from the end is used (i.e. at position -2), and so on. In other words the order of the list determines the override chain.
How is the order of the list determined?
- As plugins are discovered and modules & classes are collected, the discovered fixturedefs are appended to the list for the name.
- There is one tweak to the above: fixtures which have "no location" (registered by non-conftest plugins) are inserted before fixtures with location, so they are last in the override chain (ref).
Problem
I started looking at exposing a pytest.register_fixture function (#12376), and I want to get rid of the "has location" notion (see #12376 (comment)), because it relies on a None baseid which we want to get rid of (and always use a Node, specifically Session for current no-location fixturedefs).
There is also IMO a more conceptual problem with the mechanism: the override chain order is determined by the order in which fixtures are registered, which is kind of incidental. This is currently not really a problem in practice, since the fixture registration order mostly matches the order we want (since 46478fa improved things) -- core plugins, 3rd party plugins, the collection tree (in depth-first order). But with pytest.register_fixture we will be giving more freedom to get into odd situations, e.g.:
# conftest.py
import pytest
@pytest.hookimpl(wrapper=True)
def pytest_collection(session):
result = yield
fm = session._fixturemanager
fm._register_fixture(name="fix", func=lambda: "item", node=session.items[0])
fm._register_fixture(name="fix", func=lambda: "session", node=session)
return result
# test.py
def test_it(fix):
assert fix == "item"
I would except fix to be "item" here, because if someone registered the fixture specifically for the item, it should be preferred over some more general fixture. But currently fix is actually "session", just because of the order of the two register_fixture calls.
Proposal / idea
It seems to me that there is a natural partial order that we should impose -- the visibility of the fixturedefs. So e.g. a fixture registered on the Module will be preferred over a fixture registered on the Session`. This generalizes the somewhat clumsy "no location" ordering.
So my current idea is:
- Get rid of/deprecate FixtureDef.has_location.
- Let < be the partial order on nodes such that a node is < its ancestors. Order the fixturedef list for a name by fixturedef.node (in reverse, similar to now). For non-comparable nodes, maintain the registration order (last to register wins).
Remember that matchfactories filters out non-visible fixturedefs, so from the perspective of an item, the override chain will be strictly ordered (first by the visibility, than by registration order).
Not sure if it can be implemented cleanly and efficiently yet, I will try it soon.
Background
Pytest stores fixture definitions (FixtureDefs) for the same name in a list. When a test requests a fixture (by name), the fixturedef to use is picked as follows:
If the used fixturedef itself then requests the same fixture name, then the next one in the list from the end is used (i.e. at position -2), and so on. In other words the order of the list determines the override chain.
How is the order of the list determined?
Problem
I started looking at exposing a pytest.register_fixture function (#12376), and I want to get rid of the "has location" notion (see #12376 (comment)), because it relies on a None baseid which we want to get rid of (and always use a Node, specifically Session for current no-location fixturedefs).
There is also IMO a more conceptual problem with the mechanism: the override chain order is determined by the order in which fixtures are registered, which is kind of incidental. This is currently not really a problem in practice, since the fixture registration order mostly matches the order we want (since 46478fa improved things) -- core plugins, 3rd party plugins, the collection tree (in depth-first order). But with pytest.register_fixture we will be giving more freedom to get into odd situations, e.g.:
I would except fix to be "item" here, because if someone registered the fixture specifically for the item, it should be preferred over some more general fixture. But currently fix is actually "session", just because of the order of the two register_fixture calls.
Proposal / idea
It seems to me that there is a natural partial order that we should impose -- the visibility of the fixturedefs. So e.g. a fixture registered on the Module will be preferred over a fixture registered on the Session`. This generalizes the somewhat clumsy "no location" ordering.
So my current idea is:
Remember that matchfactories filters out non-visible fixturedefs, so from the perspective of an item, the override chain will be strictly ordered (first by the visibility, than by registration order).
Not sure if it can be implemented cleanly and efficiently yet, I will try it soon.