Skip to content

Commit 1cde9ae

Browse files
RonnyPfannschmidtCursor AIAnthropic Claude Opus 4
committed
fix: compute consistent nodeids for paths outside rootpath
Fix conftest fixture scoping when testpaths points outside rootdir (#14004). Previously, conftests outside rootpath got nodeid '' (empty string), making their fixtures global and leaking to sibling directories. Changes: - Remove initial_paths from compute_nodeid_prefix_for_path since nodeids relate to collection tree structure, not command-line paths - Use compute_nodeid_prefix_for_path in pytest_plugin_registered for consistent conftest baseid computation - Paths outside rootpath now consistently use bestrelpath from invocation_dir This ensures conftest fixtures are properly scoped regardless of whether tests are inside or outside rootpath. Co-authored-by: Cursor AI <ai@cursor.sh> Co-authored-by: Anthropic Claude Opus 4 <claude@anthropic.com>
1 parent a74d72f commit 1cde9ae

File tree

3 files changed

+17
-52
lines changed

3 files changed

+17
-52
lines changed

src/_pytest/fixtures.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1629,14 +1629,13 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin, plugin_name: str) -> N
16291629
# case-insensitive systems (Windows) and other normalization issues
16301630
# (issue #11816).
16311631
conftestpath = absolutepath(plugin_name)
1632-
try:
1633-
nodeid = str(conftestpath.parent.relative_to(self.config.rootpath))
1634-
except ValueError:
1635-
nodeid = ""
1636-
if nodeid == ".":
1637-
nodeid = ""
1638-
if os.sep != nodes.SEP:
1639-
nodeid = nodeid.replace(os.sep, nodes.SEP)
1632+
# initial_paths not available yet at plugin registration time,
1633+
# so we skip that step and fall back to bestrelpath
1634+
nodeid = nodes.compute_nodeid_prefix_for_path(
1635+
path=conftestpath.parent,
1636+
rootpath=self.config.rootpath,
1637+
invocation_dir=self.config.invocation_params.dir,
1638+
)
16401639
else:
16411640
nodeid = None
16421641

src/_pytest/nodes.py

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -622,22 +622,19 @@ def compute_nodeid_prefix_for_path(
622622
path: Path,
623623
rootpath: Path,
624624
invocation_dir: Path,
625-
initial_paths: frozenset[Path],
626625
site_packages: frozenset[Path] | None = None,
627626
) -> str:
628627
"""Compute a nodeid prefix for a filesystem path.
629628
630629
The nodeid prefix is computed based on the path's relationship to:
631630
1. rootpath - if relative, use simple relative path
632-
2. initial_paths - if relative to an initial path, use that
633-
3. site-packages - use "site://<package>/<path>" prefix
634-
4. invocation_dir - if close by, use relative path with ".." components
635-
5. Otherwise, use absolute path
631+
2. site-packages - use "site://<package>/<path>" prefix
632+
3. invocation_dir - if close by, use relative path with ".." components
633+
4. Otherwise, use absolute path
636634
637635
:param path: The path to compute a nodeid prefix for.
638636
:param rootpath: The pytest root path.
639637
:param invocation_dir: The directory from which pytest was invoked.
640-
:param initial_paths: The initial paths (testpaths or command line args).
641638
:param site_packages: Optional set of site-packages directories. If None,
642639
uses the cached system site-packages directories.
643640
@@ -653,19 +650,14 @@ def compute_nodeid_prefix_for_path(
653650
except ValueError:
654651
pass
655652

656-
# 2. Try relative to initial_paths
657-
nodeid = _check_initialpaths_for_relpath(initial_paths, path)
658-
if nodeid is not None:
659-
return nodeid.replace(os.sep, SEP) if nodeid else ""
660-
661-
# 3. Check if path is in site-packages
653+
# 2. Check if path is in site-packages
662654
site_info = _path_in_site_packages(path, site_packages)
663655
if site_info is not None:
664656
_sp_dir, rel_path = site_info
665657
result = f"site://{rel_path}"
666658
return result.replace(os.sep, SEP)
667659

668-
# 4. Try relative to invocation_dir if "close by" (i.e., not too many ".." components)
660+
# 3. Try relative to invocation_dir if "close by" (i.e., not too many ".." components)
669661
rel_from_invocation = bestrelpath(invocation_dir, path)
670662
# Count the number of ".." components - if it's reasonable, use the relative path
671663
# Also check total path length to avoid overly long relative paths
@@ -677,7 +669,7 @@ def compute_nodeid_prefix_for_path(
677669
if up_count <= 2 and rel_from_invocation != str(path):
678670
return rel_from_invocation.replace(os.sep, SEP)
679671

680-
# 5. Fall back to absolute path
672+
# 4. Fall back to absolute path
681673
return str(path).replace(os.sep, SEP)
682674

683675

@@ -725,7 +717,6 @@ def __init__(
725717
path=path,
726718
rootpath=session.config.rootpath,
727719
invocation_dir=session.config.invocation_params.dir,
728-
initial_paths=session._initialpaths,
729720
)
730721

731722
super().__init__(

testing/test_nodes.py

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -184,14 +184,13 @@ def test_compute_nodeid_inside_rootpath(self, tmp_path: Path) -> None:
184184
path=test_file,
185185
rootpath=rootpath,
186186
invocation_dir=rootpath,
187-
initial_paths=frozenset(),
188187
site_packages=frozenset(),
189188
)
190189

191190
assert result == "tests/test_foo.py"
192191

193-
def test_compute_nodeid_in_initial_paths(self, tmp_path: Path) -> None:
194-
"""Test nodeid computation for paths relative to initial_paths."""
192+
def test_compute_nodeid_outside_rootpath(self, tmp_path: Path) -> None:
193+
"""Test nodeid computation for paths outside rootpath uses bestrelpath."""
195194
rootpath = tmp_path / "project"
196195
rootpath.mkdir()
197196
tests_dir = tmp_path / "tests"
@@ -203,11 +202,11 @@ def test_compute_nodeid_in_initial_paths(self, tmp_path: Path) -> None:
203202
path=test_file,
204203
rootpath=rootpath,
205204
invocation_dir=rootpath,
206-
initial_paths=frozenset([tests_dir]),
207205
site_packages=frozenset(),
208206
)
209207

210-
assert result == "test_foo.py"
208+
# Uses bestrelpath since outside rootpath
209+
assert result == "../tests/test_foo.py"
211210

212211
def test_compute_nodeid_in_site_packages(self, tmp_path: Path) -> None:
213212
"""Test nodeid computation for paths in site-packages uses site:// prefix."""
@@ -223,7 +222,6 @@ def test_compute_nodeid_in_site_packages(self, tmp_path: Path) -> None:
223222
path=pkg_test,
224223
rootpath=rootpath,
225224
invocation_dir=rootpath,
226-
initial_paths=frozenset(),
227225
site_packages=frozenset([fake_site_packages]),
228226
)
229227

@@ -241,8 +239,6 @@ def test_compute_nodeid_nearby_relative(self, tmp_path: Path) -> None:
241239
path=sibling,
242240
rootpath=rootpath,
243241
invocation_dir=rootpath,
244-
initial_paths=frozenset(),
245-
site_packages=frozenset(),
246242
)
247243

248244
assert result == "../sibling/tests/test_foo.py"
@@ -259,8 +255,6 @@ def test_compute_nodeid_far_away_absolute(self, tmp_path: Path) -> None:
259255
path=far_away,
260256
rootpath=rootpath,
261257
invocation_dir=rootpath,
262-
initial_paths=frozenset(),
263-
site_packages=frozenset(),
264258
)
265259

266260
# Should use absolute path since it's more than 2 levels up
@@ -275,25 +269,6 @@ def test_compute_nodeid_rootpath_itself(self, tmp_path: Path) -> None:
275269
path=rootpath,
276270
rootpath=rootpath,
277271
invocation_dir=rootpath,
278-
initial_paths=frozenset(),
279-
site_packages=frozenset(),
280-
)
281-
282-
assert result == ""
283-
284-
def test_compute_nodeid_initial_path_itself(self, tmp_path: Path) -> None:
285-
"""Test nodeid computation for initial_path itself returns empty string."""
286-
rootpath = tmp_path / "project"
287-
rootpath.mkdir()
288-
tests_dir = tmp_path / "tests"
289-
tests_dir.mkdir()
290-
291-
result = nodes.compute_nodeid_prefix_for_path(
292-
path=tests_dir,
293-
rootpath=rootpath,
294-
invocation_dir=rootpath,
295-
initial_paths=frozenset([tests_dir]),
296-
site_packages=frozenset(),
297272
)
298273

299274
assert result == ""

0 commit comments

Comments
 (0)