fix #14004 - connect conftests to nodeids/nodes instead of matching string prefixes - ensure we connect in the collect phase in case of directory confusion#14098
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes conftest fixture scoping when testpaths points outside rootdir using relative paths (issue #14004). The fix migrates from fragile string prefix matching to robust node-based matching for fixture scoping. Conftest fixtures are now parsed during Directory collection, using the Directory node's nodeid for proper scoping instead of calculating string-based nodeids during plugin registration.
- Implements deferred conftest parsing tied to Directory collection
- Adds node-based fixture matching alongside string-based fallback for compatibility
- Deprecates
baseid/nodeidstring parameters in favor ofnodeparameter
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| testing/test_conftest.py | Adds comprehensive regression test for fixture scoping with testpaths outside rootdir |
| src/_pytest/fixtures.py | Implements deferred conftest parsing, node-based matching, and deprecates string-based APIs |
| src/_pytest/unittest.py | Updates fixture registration calls to use new node parameter |
| src/_pytest/python.py | Updates fixture registration calls to use new node parameter |
| changelog/14004.deprecation.rst | Documents deprecation of baseid/nodeid string parameters |
| changelog/14004.bugfix.rst | Documents the fixture scoping fix |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Interesting! The "tests outside of the rootdir" scenario is ill-advised but I guess we need to support it. Regarding the first commit, I'm looking at the test <Dir tests> '::tests'
<Dir sdk> ''
<Dir inner> 'inner'
<Module test_inner.py> 'inner/test_inner.py'
<Function test_inner> 'inner/test_inner.py::test_inner'
<Module test_outer.py> 'test_outer.py'
<Function test_outer> 'test_outer.py::test_outer'That seems odd and broken to me, there's obviously a confusion going on here. Generally the intention is for nodeid's to be relative to the rootdir, so what happens when the collection path is not under the rootdir? I've already written about this issue in #11245. The commit "fix #14004 - connect conftests to nodeids/nodes instead of matching string prefixes - ensure we connect in the collect phase in case of directory confusion" works around the issue using some "fixup" code in fixtures.py. I think it's a bit confusing, particularly the function name "_get_nodeid_for_path_outside_rootpath", it basically invents a "fake" nodeid because the existing nodeid is broken, but it's not an actual ID of a node... I think that if we fix the underlying issue #11245, that would be a better way to go? In that issue we had a couple of suggestions. Also regarding the implementation itself, I fear that the loop over the Regarding the "fix: assign conftest fixtures to Directory nodes during collection" idea and subsequent changes, I think I like the direction, I definitely think we should to try to integrate conftests with the collection tree better, and get away from the current path matching. Thanks for taking a look at this. I need to look into it more, but can you describe the interaction with initial conftests? Are they associated with a Directory or are they "global"? The Lines 383 to 399 in 0e9db5f |
|
the main confusion thats happening is that initial conftest files are loaded before we have a node, so we cant parse factories until we see them again in collection the case of tets outside of a rootdir is not as ill advised as one might think the moment one ships integration testsuites as installed python packages and suddenly many tests collect as part of site-packages instead of the cwd |
|
The "add: nodes.norm_sep helper for nodeid path separator normalization" change looks sensible, you can submit and merge it to main with my review if you'd like. As an internal refactoring it doesn't need a changelog entry though. Can you submit "improve: compute meaningful nodeids for paths outside rootdir" in its own PR? I have some thoughts but it's a substantial change so will be better considered separately before this PR. |
But do all initial conftests will have a node? Conftests are looked up the filesystem tree (up to confcutdir) while nodes are not. So I figure it's possible to have a conftest plugin without a corresponding Directory, though I haven't verified this so might be wrong about that. |
|
As far as I understand any conftest not having a attached node isn't a valid fixture source |
|
You may be right but that would surprise me! I'd imagine these fixtures would be "global" (empty/None baseid, don't remember which one of them). Are you able to verify this? |
|
a conftest is a plugin that is local to a directory tree, the only reason we do initial conftests to begin with is ci arg and config value handling for full testsuites - so no - a conftest should never be a global plugi never |
9f24dcd to
585cdc4
Compare
I don't know, I think having the ability for a project to locally define fixtures with global visibility is useful, even necessary. I agree it's a bit unfortunate to conflate it with conftests which are meant to be Directory-local, but initial conftests is what we have and I'm not sure it's worth it to add another mechanism1. So I think my vote is to give initial conftests global visibility (i.e. their node would be Footnotes
|
|
that would stil lbe a attached node for me - im absolutely fine with making initial conftests something for the session/root dirrectory - but i di suspect edge cases coming up |
|
OK, great. Overall, I think attaching conftests and FixtureDefs explicitly to nodes will be a great step in bringing more clarity to pytest's core model. So if I have time I'd like to look into it more, maybe using this PR as a starting point. |
|
I looked at it a bit. I wanted to start with conftest <-> Directory change. As mentioned earlier, I really want to avoid the For non-initial nodes it's not a problem, we already have the Directory node as we are loading the conftest during collection (ref). For initial conftests, the loading is done before collection so we don't have Directory nodes, and my idea above was to use Due to the way initial conftests are currently decided, I think that might be very confusing. Specifically, if the user passes a directory arg to Really, I think the problem with this one is not the global visibility idea, but the fact that we even consider random directory args as initial conftest candidates. To me it seems incoherent that a selection arg can affect whether a conftest is initial or not. I might open an issue about this. The two other places where conftest path is used are:
The thing to decide here is whether the hook visibility of initial fixtures will also become global, for consistency with fixture visibility. I think we do want them to be consistent, so if the answer here is "no", then I think we should scrape the idea. If we make initial conftest loading more sane as discussed in the previous point, I think it might be sensible to make initial conftest have global hook-visibility. So I'll defer until we decide on that. Expanding from the previous point a bit, in a future where we strictly attach conftests to nodes, we should decide between two models:
The first is what we do now. In practical terms, when we look up conftests which affect something, we do it by path. The second would mean that once a conftest is attached to a node, we completely "forget" the conftest's path, and when looking up conftests, we always do it by node. Philosophically I much prefer to lean in fully on the collection tree. Seems much more "pytest native" and clean, than to keep relying on the parallel filesystem tree post-collection. For common usage in practice the two would be mostly equivalent, but the second gives plugins more room to play. Happy to hear opinions. |
|
by path is broken and not fixable in a sensible manner (i tried in the quickfix and badly failed running into one edge case after another) my understanding of conftests has always been that they are plugins local do a collection tree, their behaviour got strange when directoy nodes got removed initially - now that they are back we can undo that the pending check is necessary as initial conftests currently exist before initial nodes, im happy to make that unnecesary, but right now its not a simple/feasible change - in between initial conftests and plugin fixture parsing in future i expect to move fixture parsing entirely to collection time and mapping conftests there without the need for pending - but the requried changes are something i consider out of scope for this pr |
|
I didn't have time to read the PR code (sorry about that), but wanted to comment on #14098 (comment):
I agree completely, this always felt like a wart to me. At work, we put the source code in a deeply nested directory, The special rules around initial conftests are really confusing and hard to explain to newcomers.
(I assume you mean "initial conftests" above). I don't think fixtures should become global, as much as I understand the "consistency" reationale.
I agree, we should definitely do that. Theoretically, it should be possible to have fixtures and tests which are not even associated with a file system path.
Also my understanding. Seems like "contests as a directory-local mini plugin" is an easy concept to understand and explain. On the other hand "initial conftests" always seems esoteric and hard to understand (so much I did not find anything in the docs that clearly explain what they are). |
|
im of the impression that initial conftests should be deprecated and we should have a better way to spell out per project local plug-in that arent conftests i vaguely recall that in the early days - ALL conftests where always added as plugins - and it was a disaster this could play into my proposal for testroots - having a actual config that sets up initial collection defaults and initial local plugins |
a1ac5e1 to
f6a6122
Compare
|
@nicoddemus @bluetech please have another look |
|
Thanks for the update. I've come to terms with I'd like to discuss the deprecations. Basically I think we should skip the deprecation here, as these are internal APIs. It will break some plugins, but plugins which use private details should be ready for this. Normally I wouldn't object to a deprecation, I don't mean to make life unnecessarily hard for plugins, but just in this case the need to keep supporting nodeids until pytest 10 will block further progress. As a small consolation I'm pretty sure this change will unlock stabilizing a If you feel strongly about the deprecation, I'll be happy to approve with it, but hopefully I can convince you :) |
|
i absolutely want to replace pending/initial conftests by something akin to my collection roots proposal - but that's a bit further away i consider the deprecation a important goodwill to plug-ins like pytest-cases and a few more |
| Conftest fixtures are now parsed during Directory collection, using the Directory node's | ||
| nodeid for proper scoping. |
There was a problem hiding this comment.
Conftest fixtures are now parsed during :class:Directory <pytest.Directory> collection, using the Directory node for proper scoping.
| Passing ``baseid`` to :class:`~pytest.FixtureDef` or ``nodeid`` strings to fixture registration | ||
| APIs is now deprecated. |
There was a problem hiding this comment.
| Passing ``baseid`` to :class:`~pytest.FixtureDef` or ``nodeid`` strings to fixture registration | |
| APIs is now deprecated. | |
| Passing ``baseid`` to :class:`~pytest.FixtureDef` or ``nodeid`` strings to fixture registration APIs is now deprecated. These are internal pytest APIs that are used by some plugins. |
| # baseid=None (global plugins) and baseid="" (synthetic fixtures) are fine. | ||
| if baseid and node is None: | ||
| warnings.warn( | ||
| "Passing baseid to FixtureDef is deprecated. " |
There was a problem hiding this comment.
Can you move the Warning to a constant in deprecated.py? It makes it easier on major releases to track the deprecations.
| # nodeid=None (global plugins) is fine. | ||
| if nodeid and node is None: | ||
| warnings.warn( | ||
| "Passing nodeid to _register_fixture is deprecated. " |
There was a problem hiding this comment.
Same here, can you move to a constant in deprecated.py?
| # Global plugin autouse fixtures go under Session. | ||
| self._node_autousenames.setdefault(self.session, []).append(name) |
There was a problem hiding this comment.
This is not backward compatible when passing nodeid. Previously would have nodeid autouse visibility, now session. Maybe it'd be filtered anyway in matchfactories? But still seems undesirable.
There was a problem hiding this comment.
nodeid or "" is kind of an equivalent of node or session
but plain nodeids in that mapping need some exta consideration
|
One more thing I forgot, we should add an entry in deprecations.rst. |
Fixes pytest-dev#14004 - conftest fixtures now properly scoped when testpaths points outside rootdir. Instead of computing fixture nodeids from path strings during plugin registration, conftest fixtures are now deferred until their Directory is collected. The Directory node is stored on FixtureDef and used for node-identity matching, which is more robust than string prefix matching. Changes: - Add _pending_conftests dict to defer conftest fixture parsing - Parse conftest fixtures via pytest_make_collect_report hook - Add node parameter to FixtureDef and _register_fixture - Add parsefactories(holder=, node=) keyword-only API - Use node-based matching in _matchfactories with string fallback Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
…ration Update all internal call sites to use node= parameter instead of nodeid= string for fixture registration, enabling node-based matching. Changes: - python.py: Module and Class xunit fixtures now use node=self - python.py: Class.collect parsefactories uses holder=..., node=self - unittest.py: UnitTestCase fixtures now use node=self - unittest.py: UnitTestCase.collect parsefactories uses holder=..., node=self Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
…meters Add deprecation warnings for string-based fixture scoping APIs: - FixtureDef baseid parameter: use node parameter instead - _register_fixture nodeid parameter: use node parameter instead - parsefactories nodeid string: use parsefactories(holder=, node=) instead Warnings only trigger when a non-empty nodeid string is passed without a node. Global plugins (nodeid=None) and synthetic fixtures (baseid='') do not trigger warnings. Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
- Refactor _nodeid_autousenames to _node_autousenames: use Node objects instead of nodeid strings to prevent duplicates and improve consistency - Flush above-rootdir conftests to Session scope in __init__ rather than in pytest_make_collect_report, ensuring they're processed even when collection fails before Session collection starts (e.g. bad args) - Add pytest_collection_finish cleanup for interrupted-collection leftovers - Add regression test for pytest-dev#14004 (conftest fixture leak across siblings) - Add test for ancestor conftests above rootdir via confcutdir Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
- Use :confval:, :ref:, and :class: rst formatting in changelog - Note that deprecated APIs are internal, used by some plugins - Move deprecation warnings to constants in deprecated.py for easier tracking - Fix backward compat: legacy nodeid-based autouse scoping preserved via _nodeid_autousenames fallback dict instead of falling through to Session Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
b07ea7d to
a2398a7
Compare
fixes #14004
the key issue was that
nodeid/baseidfor fixtures of suchconftest.pywould be""instead of something fittingths pr is a chain of changes