-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Fix conftest fixture scoping when testpaths points outside rootdir #14118
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| Fixed conftest.py fixture scoping when ``testpaths`` points outside ``rootdir``. | ||
|
|
||
| Nodeids for paths outside ``rootdir`` are now computed more meaningfully: | ||
| paths in site-packages use a ``site://`` prefix, nearby paths use relative | ||
| paths with ``..`` components, and far-away paths use absolute paths. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -38,6 +38,7 @@ | |||||||||||||||||||||||||||||||||||||||||||
| from _pytest.mark.structures import NodeKeywords | ||||||||||||||||||||||||||||||||||||||||||||
| from _pytest.outcomes import fail | ||||||||||||||||||||||||||||||||||||||||||||
| from _pytest.pathlib import absolutepath | ||||||||||||||||||||||||||||||||||||||||||||
| from _pytest.pathlib import bestrelpath | ||||||||||||||||||||||||||||||||||||||||||||
| from _pytest.stash import Stash | ||||||||||||||||||||||||||||||||||||||||||||
| from _pytest.warning_types import PytestWarning | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -557,6 +558,121 @@ def _check_initialpaths_for_relpath( | |||||||||||||||||||||||||||||||||||||||||||
| return None | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| def _get_site_packages_dirs() -> frozenset[Path]: | ||||||||||||||||||||||||||||||||||||||||||||
| """Get all site-packages directories as resolved absolute paths.""" | ||||||||||||||||||||||||||||||||||||||||||||
| import site | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| dirs: set[Path] = set() | ||||||||||||||||||||||||||||||||||||||||||||
| # Standard site-packages | ||||||||||||||||||||||||||||||||||||||||||||
| for sp in site.getsitepackages(): | ||||||||||||||||||||||||||||||||||||||||||||
| try: | ||||||||||||||||||||||||||||||||||||||||||||
| dirs.add(Path(sp).resolve()) | ||||||||||||||||||||||||||||||||||||||||||||
| except OSError: | ||||||||||||||||||||||||||||||||||||||||||||
| pass | ||||||||||||||||||||||||||||||||||||||||||||
| # User site-packages | ||||||||||||||||||||||||||||||||||||||||||||
| user_site = site.getusersitepackages() | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+567
to
+573
|
||||||||||||||||||||||||||||||||||||||||||||
| for sp in site.getsitepackages(): | |
| try: | |
| dirs.add(Path(sp).resolve()) | |
| except OSError: | |
| pass | |
| # User site-packages | |
| user_site = site.getusersitepackages() | |
| try: | |
| site_packages = site.getsitepackages() | |
| except AttributeError: | |
| site_packages = [] | |
| for sp in site_packages: | |
| try: | |
| dirs.add(Path(sp).resolve()) | |
| except OSError: | |
| pass | |
| # User site-packages | |
| try: | |
| user_site = site.getusersitepackages() | |
| except AttributeError: | |
| user_site = None |
Copilot
AI
Jan 17, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -138,3 +138,137 @@ def test_show_wrong_path(private_dir): | |
| ) | ||
| result = pytester.runpytest() | ||
| result.stdout.fnmatch_lines([str(p) + ":*: AssertionError", "*1 failed in *"]) | ||
|
|
||
|
|
||
| class TestNodeidPrefixComputation: | ||
| """Tests for nodeid prefix computation for paths outside rootdir.""" | ||
|
|
||
| def test_path_in_site_packages_found(self, tmp_path: Path) -> None: | ||
| """Test _path_in_site_packages finds paths inside site-packages.""" | ||
| fake_site_packages = tmp_path / "site-packages" | ||
| fake_site_packages.mkdir() | ||
| pkg_path = fake_site_packages / "mypackage" / "tests" / "test_foo.py" | ||
| pkg_path.parent.mkdir(parents=True) | ||
| pkg_path.touch() | ||
|
|
||
| site_packages = frozenset([fake_site_packages]) | ||
| result = nodes._path_in_site_packages(pkg_path, site_packages) | ||
|
|
||
| assert result is not None | ||
| sp_dir, rel_path = result | ||
| assert sp_dir == fake_site_packages | ||
| assert rel_path == Path("mypackage/tests/test_foo.py") | ||
|
|
||
| def test_path_in_site_packages_not_found(self, tmp_path: Path) -> None: | ||
| """Test _path_in_site_packages returns None for paths outside site-packages.""" | ||
| fake_site_packages = tmp_path / "site-packages" | ||
| fake_site_packages.mkdir() | ||
| other_path = tmp_path / "other" / "test_foo.py" | ||
| other_path.parent.mkdir(parents=True) | ||
| other_path.touch() | ||
|
|
||
| site_packages = frozenset([fake_site_packages]) | ||
| result = nodes._path_in_site_packages(other_path, site_packages) | ||
|
|
||
| assert result is None | ||
|
|
||
| def test_compute_nodeid_inside_rootpath(self, tmp_path: Path) -> None: | ||
| """Test nodeid computation for paths inside rootpath.""" | ||
| rootpath = tmp_path / "project" | ||
| rootpath.mkdir() | ||
| test_file = rootpath / "tests" / "test_foo.py" | ||
| test_file.parent.mkdir(parents=True) | ||
| test_file.touch() | ||
|
|
||
| result = nodes.compute_nodeid_prefix_for_path( | ||
| path=test_file, | ||
| rootpath=rootpath, | ||
| invocation_dir=rootpath, | ||
| site_packages=frozenset(), | ||
| ) | ||
|
|
||
| assert result == "tests/test_foo.py" | ||
|
|
||
| def test_compute_nodeid_outside_rootpath(self, tmp_path: Path) -> None: | ||
| """Test nodeid computation for paths outside rootpath uses bestrelpath.""" | ||
| rootpath = tmp_path / "project" | ||
| rootpath.mkdir() | ||
| tests_dir = tmp_path / "tests" | ||
| tests_dir.mkdir() | ||
| test_file = tests_dir / "test_foo.py" | ||
| test_file.touch() | ||
|
|
||
| result = nodes.compute_nodeid_prefix_for_path( | ||
| path=test_file, | ||
| rootpath=rootpath, | ||
| invocation_dir=rootpath, | ||
| site_packages=frozenset(), | ||
| ) | ||
|
|
||
| # Uses bestrelpath since outside rootpath | ||
| assert result == "../tests/test_foo.py" | ||
|
|
||
| def test_compute_nodeid_in_site_packages(self, tmp_path: Path) -> None: | ||
| """Test nodeid computation for paths in site-packages uses site:// prefix.""" | ||
| rootpath = tmp_path / "project" | ||
| rootpath.mkdir() | ||
| fake_site_packages = tmp_path / "site-packages" | ||
| fake_site_packages.mkdir() | ||
| pkg_test = fake_site_packages / "mypackage" / "tests" / "test_foo.py" | ||
| pkg_test.parent.mkdir(parents=True) | ||
| pkg_test.touch() | ||
|
|
||
| result = nodes.compute_nodeid_prefix_for_path( | ||
| path=pkg_test, | ||
| rootpath=rootpath, | ||
| invocation_dir=rootpath, | ||
| site_packages=frozenset([fake_site_packages]), | ||
| ) | ||
|
|
||
| assert result == "site://mypackage/tests/test_foo.py" | ||
|
|
||
| def test_compute_nodeid_nearby_relative(self, tmp_path: Path) -> None: | ||
| """Test nodeid computation for nearby paths uses relative path.""" | ||
| rootpath = tmp_path / "project" | ||
| rootpath.mkdir() | ||
| sibling = tmp_path / "sibling" / "tests" / "test_foo.py" | ||
| sibling.parent.mkdir(parents=True) | ||
| sibling.touch() | ||
|
|
||
| result = nodes.compute_nodeid_prefix_for_path( | ||
| path=sibling, | ||
| rootpath=rootpath, | ||
| invocation_dir=rootpath, | ||
| ) | ||
|
|
||
| assert result == "../sibling/tests/test_foo.py" | ||
|
|
||
| def test_compute_nodeid_far_away_absolute(self, tmp_path: Path) -> None: | ||
| """Test nodeid computation for far-away paths uses absolute path.""" | ||
| rootpath = tmp_path / "deep" / "nested" / "project" | ||
| rootpath.mkdir(parents=True) | ||
| far_away = tmp_path / "other" / "location" / "tests" / "test_foo.py" | ||
| far_away.parent.mkdir(parents=True) | ||
| far_away.touch() | ||
|
|
||
| result = nodes.compute_nodeid_prefix_for_path( | ||
| path=far_away, | ||
| rootpath=rootpath, | ||
| invocation_dir=rootpath, | ||
| ) | ||
|
|
||
| # Should use absolute path since it's more than 2 levels up | ||
| assert result == str(far_away) | ||
|
||
|
|
||
| def test_compute_nodeid_rootpath_itself(self, tmp_path: Path) -> None: | ||
| """Test nodeid computation for rootpath itself returns empty string.""" | ||
| rootpath = tmp_path / "project" | ||
| rootpath.mkdir() | ||
|
|
||
| result = nodes.compute_nodeid_prefix_for_path( | ||
| path=rootpath, | ||
| rootpath=rootpath, | ||
| invocation_dir=rootpath, | ||
| ) | ||
|
|
||
| assert result == "" | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'except' clause does nothing but pass and there is no explanatory comment.