/
niceSOFT
/
python3-setuptools_scm
Обзор
Документация
Войти
/
niceSOFT
/
python3-setuptools_scm
Код
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
setuptools-scm/testing_scm/test_integration.py
1 907 строк
61 KB
pre-commit-ci[bot]
style: pre-commit fixes
03 авг 2026, 21:57
03 авг 2026, 21:57
8a32949
Код
Авторство
О чём код?
from __future__ import annotations import importlib.metadata import logging import os import re import subprocess import sys import textwrap from pathlib import Path from typing import TYPE_CHECKING from typing import Any import pytest from packaging.version import Version from setuptools_scm._integration import setuptools as setuptools_integration from setuptools_scm._integration.pyproject_reading import PyProjectData from setuptools_scm._integration.setup_cfg import SetuptoolsBasicData from setuptools_scm._integration.setup_cfg import read_setup_cfg from vcs_versioning._requirement_cls import extract_package_name if TYPE_CHECKING: import setuptools from setuptools_scm import Configuration from vcs_versioning._overrides import PRETEND_KEY from vcs_versioning._overrides import PRETEND_KEY_NAMED from vcs_versioning._run_cmd import run from vcs_versioning.test_api import WorkDir c = Configuration() def _run_setuptools_setup(cwd: Path) -> subprocess.CompletedProcess[str]: """Invoke ``setuptools.setup()`` with no commands to trigger inference hooks. Exits non-zero ("no commands supplied") but the hooks still fire, writing version files to the source tree as a side effect. """ result = subprocess.run( [sys.executable, "-c", "import setuptools; setuptools.setup()"], cwd=cwd, capture_output=True, text=True, check=False, ) assert result.returncode != 0, "setup() with no commands should exit non-zero" return result # Module-level fixture for git SCM setup @pytest.fixture def wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> WorkDir: """Set up git for integration tests.""" return wd.setup_git(monkeypatch) @pytest.mark.issue("1314") def test_get_version_no_implicit_global_overrides_warning_subprocess( wd: WorkDir, ) -> None: """setuptools_scm.get_version must not rely on implicit GlobalOverrides auto-create. Pytest's vcs_versioning plugin installs a global ``GlobalOverrides`` context, so this check runs in a subprocess without that fixture (see GitHub issue #1314). """ root = os.fspath(wd.cwd) script = textwrap.dedent( f""" import warnings warnings.filterwarnings( "error", message="No GlobalOverrides context is active", category=UserWarning, ) import setuptools_scm setuptools_scm.get_version(root={root!r}) """ ) proc = subprocess.run( [sys.executable, "-c", script], capture_output=True, text=True, check=False, ) assert proc.returncode == 0, (proc.stdout, proc.stderr) def test_pyproject_support(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("SETUPTOOLS_SCM_DEBUG") pkg = tmp_path / "package" pkg.mkdir() pkg.joinpath("pyproject.toml").write_text( textwrap.dedent( """ [tool.setuptools_scm] fallback_version = "12.34" [project] name = "foo" description = "Factory ⸻ A code generator 🏭" authors = [{name = "Łukasz Langa"}] dynamic = ["version"] """ ), encoding="utf-8", ) pkg.joinpath("setup.py").write_text( "__import__('setuptools').setup()", encoding="utf-8" ) res = run([sys.executable, "setup.py", "--version"], pkg) assert res.stdout == "12.34" def test_pretend_version(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None: monkeypatch.setenv(PRETEND_KEY, "1.0.0") assert wd.get_version() == "1.0.0" assert wd.get_version(dist_name="ignored") == "1.0.0" def test_pretend_version_named(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None: monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test".upper()), "1.0.0") monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test2".upper()), "2.0.0") assert wd.get_version(dist_name="test") == "1.0.0" assert wd.get_version(dist_name="test2") == "2.0.0" def test_pretend_version_name_takes_precedence( monkeypatch: pytest.MonkeyPatch, wd: WorkDir ) -> None: monkeypatch.setenv(PRETEND_KEY_NAMED.format(name="test".upper()), "1.0.0") monkeypatch.setenv(PRETEND_KEY, "2.0.0") assert wd.get_version(dist_name="test") == "1.0.0" def test_pretend_version_rejects_invalid_string( monkeypatch: pytest.MonkeyPatch, wd: WorkDir ) -> None: """Test that invalid pretend versions raise errors and bubble up.""" monkeypatch.setenv(PRETEND_KEY, "dummy") # With strict validation, invalid pretend versions should raise errors with pytest.raises(Exception, match=r".*dummy.*"): wd.get_version(write_to="test.py") def test_pretend_metadata_with_version( monkeypatch: pytest.MonkeyPatch, wd: WorkDir ) -> None: """Test pretend metadata overrides work with pretend version.""" from vcs_versioning._overrides import PRETEND_METADATA_KEY monkeypatch.setenv(PRETEND_KEY, "1.2.3.dev4+g1337beef") monkeypatch.setenv(PRETEND_METADATA_KEY, '{node="g1337beef", distance=4}') version = wd.get_version() assert version == "1.2.3.dev4+g1337beef" # Test version file template functionality wd("mkdir -p src") version_file_content = """ version = '{version}' major = {version_tuple[0]} minor = {version_tuple[1]} patch = {version_tuple[2]} commit_hash = '{scm_version.short_node}' num_commit = {scm_version.distance} """ # ruff: ignore[missing-f-string-syntax] # Use write_to with template to create version file version = wd.get_version( write_to="src/version.py", write_to_template=version_file_content ) content = (wd.cwd / "src/version.py").read_text(encoding="utf-8") assert "commit_hash = 'g1337beef'" in content assert "num_commit = 4" in content def test_pretend_metadata_named(monkeypatch: pytest.MonkeyPatch, wd: WorkDir) -> None: """Test pretend metadata with named package support.""" from vcs_versioning._overrides import PRETEND_METADATA_KEY_NAMED monkeypatch.setenv( PRETEND_KEY_NAMED.format(name="test".upper()), "1.2.3.dev5+gabcdef12" ) monkeypatch.setenv( PRETEND_METADATA_KEY_NAMED.format(name="test".upper()), '{node="gabcdef12", distance=5, dirty=true}', ) version = wd.get_version(dist_name="test") assert version == "1.2.3.dev5+gabcdef12" def test_pretend_metadata_without_version_warns( monkeypatch: pytest.MonkeyPatch, wd: WorkDir, caplog: pytest.LogCaptureFixture ) -> None: """Test that pretend metadata without any base version logs a warning.""" from vcs_versioning._overrides import PRETEND_METADATA_KEY # Only set metadata, no version - but there will be a git repo so there will be a base version # Let's create an empty git repo without commits to truly have no base version monkeypatch.setenv(PRETEND_METADATA_KEY, '{node="g1234567", distance=2}') with caplog.at_level(logging.WARNING): version = wd.get_version() assert version is not None # In this case, metadata was applied to a fallback version, so no warning about missing base def test_pretend_metadata_with_scm_version( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: """Test that pretend metadata works with actual SCM-detected version.""" from vcs_versioning._overrides import PRETEND_METADATA_KEY # Set up a git repo with a tag so we have a base version wd("git init") wd("git config user.name test") wd("git config user.email test@example.com") wd.write("file.txt", "content") wd("git add file.txt") wd("git commit -m 'initial'") wd("git tag v1.0.0") # Now add metadata overrides monkeypatch.setenv(PRETEND_METADATA_KEY, '{node="gcustom123", distance=7}') # Test that the metadata gets applied to the actual SCM version version = wd.get_version() # The version becomes 1.0.1.dev7+gcustom123 due to version scheme and metadata overrides assert "1.0.1.dev7+gcustom123" == version # Test version file to see if metadata was applied wd("mkdir -p src") version_file_content = """ version = '{version}' commit_hash = '{scm_version.short_node}' num_commit = {scm_version.distance} """ # ruff: ignore[missing-f-string-syntax] version = wd.get_version( write_to="src/version.py", write_to_template=version_file_content ) content = (wd.cwd / "src/version.py").read_text(encoding="utf-8") assert "commit_hash = 'gcustom123'" in content assert "num_commit = 7" in content def test_pretend_metadata_type_conversion( monkeypatch: pytest.MonkeyPatch, wd: WorkDir ) -> None: """Test that pretend metadata properly uses TOML native types.""" from vcs_versioning._overrides import PRETEND_METADATA_KEY monkeypatch.setenv(PRETEND_KEY, "2.0.0") monkeypatch.setenv( PRETEND_METADATA_KEY, '{distance=10, dirty=true, node="gfedcba98", branch="feature-branch"}', ) version = wd.get_version() # The version should be formatted properly with the metadata assert "2.0.0" in version def test_pretend_metadata_invalid_fields_filtered( monkeypatch: pytest.MonkeyPatch, wd: WorkDir, caplog: pytest.LogCaptureFixture ) -> None: """Test that invalid metadata fields are filtered out with a warning.""" from vcs_versioning._overrides import PRETEND_METADATA_KEY monkeypatch.setenv(PRETEND_KEY, "1.0.0") monkeypatch.setenv( PRETEND_METADATA_KEY, '{node="g123456", distance=3, invalid_field="should_be_ignored", another_bad_field=42}', ) with caplog.at_level(logging.WARNING): version = wd.get_version() assert version == "1.0.0" assert "Invalid fields in TOML data" in caplog.text assert "invalid_field" in caplog.text assert "another_bad_field" in caplog.text def test_pretend_metadata_date_parsing( monkeypatch: pytest.MonkeyPatch, wd: WorkDir ) -> None: """Test that TOML date values work in pretend metadata.""" from vcs_versioning._overrides import PRETEND_METADATA_KEY monkeypatch.setenv(PRETEND_KEY, "1.5.0") monkeypatch.setenv( PRETEND_METADATA_KEY, '{node="g987654", distance=7, node_date=2024-01-15}' ) version = wd.get_version() assert version == "1.5.0" def test_pretend_metadata_invalid_toml_error( monkeypatch: pytest.MonkeyPatch, wd: WorkDir, caplog: pytest.LogCaptureFixture ) -> None: """Test that invalid TOML in pretend metadata logs an error.""" from vcs_versioning._overrides import PRETEND_METADATA_KEY monkeypatch.setenv(PRETEND_KEY, "1.0.0") monkeypatch.setenv(PRETEND_METADATA_KEY, "{invalid toml syntax here}") with caplog.at_level(logging.ERROR): version = wd.get_version() # Should fall back to basic pretend version assert version == "1.0.0" assert "Failed to parse pretend metadata" in caplog.text def test_git_tag_with_local_build_data_preserved(wd: WorkDir) -> None: """Test that git tags containing local build data are preserved in final version.""" wd.commit_testfile() # Create a git tag that includes local build data # This simulates a CI system that creates tags with build metadata wd("git tag 1.0.0+build.123") # The version should preserve the build metadata from the tag version = wd.get_version() # Validate it's a proper PEP 440 version parsed_version = Version(version) assert str(parsed_version) == version, ( f"Version should parse correctly as PEP 440: {version}" ) assert version == "1.0.0+build.123", ( f"Expected build metadata preserved, got {version}" ) # Validate the local part is correct assert parsed_version.local == "build.123", ( f"Expected local part 'build.123', got {parsed_version.local}" ) def test_git_tag_with_commit_hash_preserved(wd: WorkDir) -> None: """Test that git tags with commit hash data are preserved.""" wd.commit_testfile() # Create a git tag that includes commit hash metadata wd("git tag 2.0.0+sha.abcd1234") # The version should preserve the commit hash from the tag version = wd.get_version() # Validate it's a proper PEP 440 version parsed_version = Version(version) assert str(parsed_version) == version, ( f"Version should parse correctly as PEP 440: {version}" ) assert version == "2.0.0+sha.abcd1234" # Validate the local part is correct assert parsed_version.local == "sha.abcd1234", ( f"Expected local part 'sha.abcd1234', got {parsed_version.local}" ) def test_git_tag_with_local_build_data_preserved_dirty_workdir(wd: WorkDir) -> None: """Test that git tags with local build data are preserved even with dirty working directory.""" wd.commit_testfile() # Create a git tag that includes local build data wd("git tag 1.5.0+build.456") # Make working directory dirty wd.write("modified_file.txt", "some changes") # The version should preserve the build metadata from the tag # even when working directory is dirty version = wd.get_version() # Validate it's a proper PEP 440 version parsed_version = Version(version) assert str(parsed_version) == version, ( f"Version should parse correctly as PEP 440: {version}" ) assert version == "1.5.0+build.456", ( f"Expected build metadata preserved with dirty workdir, got {version}" ) # Validate the local part is correct assert parsed_version.local == "build.456", ( f"Expected local part 'build.456', got {parsed_version.local}" ) def test_git_tag_with_local_build_data_preserved_with_distance(wd: WorkDir) -> None: """Test that git tags with local build data are preserved with distance.""" wd.commit_testfile() # Create a git tag that includes local build data wd("git tag 3.0.0+ci.789") # Add another commit after the tag to create distance wd.commit_testfile("after-tag") # The version should use version scheme for distance but preserve original tag's build data version = wd.get_version() # Validate it's a proper PEP 440 version parsed_version = Version(version) assert str(parsed_version) == version, ( f"Version should parse correctly as PEP 440: {version}" ) # Tag local data should be preserved and combined with SCM data assert version.startswith("3.0.1.dev1"), ( f"Expected dev version with distance, got {version}" ) # Use regex to validate the version format with both tag build data and SCM node data # Expected format: 3.0.1.dev1+ci.789.g<commit_hash> version_pattern = r"^3\.0\.1\.dev1\+ci\.789\.g[a-f0-9]+$" assert re.match(version_pattern, version), ( f"Version should match pattern {version_pattern}, got {version}" ) # The original tag's local data (+ci.789) should be preserved and combined with SCM data assert "+ci.789" in version, f"Tag local data should be preserved, got {version}" # Validate the local part contains both tag and SCM node information assert parsed_version.local is not None, ( f"Expected local version part, got {parsed_version.local}" ) assert "ci.789" in parsed_version.local, ( f"Expected local part to contain tag data 'ci.789', got {parsed_version.local}" ) assert "g" in parsed_version.local, ( f"Expected local part to contain SCM node data 'g...', got {parsed_version.local}" ) # Note: This test verifies that local build data from tags is preserved and combined # with SCM data when there's distance, which is the desired behavior for issue 1019. @pytest.mark.issue(611) def test_distribution_provides_extras() -> None: from importlib.metadata import distribution dist = distribution("setuptools_scm") pe: list[str] = dist.metadata.get_all("Provides-Extra", []) assert sorted(pe) == ["rich", "simple", "toml"] @pytest.mark.issue(760) def test_unicode_in_setup_cfg(tmp_path: Path) -> None: cfg = tmp_path / "setup.cfg" cfg.write_text( textwrap.dedent( """ [metadata] name = configparser author = Łukasz Langa """ ), encoding="utf-8", ) from setuptools_scm._integration.setup_cfg import read_setup_cfg name = read_setup_cfg(cfg).name assert name == "configparser" # also ensure we can parse a version if present (legacy projects) cfg.write_text( textwrap.dedent( """ [metadata] name = configparser version = 1.2.3 """ ), encoding="utf-8", ) data = read_setup_cfg(cfg) assert isinstance(data, SetuptoolsBasicData) assert data.name == "configparser" assert data.version == "1.2.3" @pytest.mark.issue(1216) def test_setup_cfg_dynamic_version_warns_and_ignores(tmp_path: Path) -> None: cfg = tmp_path / "setup.cfg" cfg.write_text( textwrap.dedent( """ [metadata] name = example-broken version = attr: example_broken.__version__ """ ), encoding="utf-8", ) with pytest.warns( UserWarning, match=r"setup\.cfg: at \[metadata\]", ): legacy_data = read_setup_cfg(cfg) assert legacy_data.version is None def test_setup_cfg_version_prevents_inference_version_keyword( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: # Legacy project setup - we construct the data directly since files are not read anyway monkeypatch.chdir(tmp_path) dist = create_clean_distribution("legacy-proj") # Using keyword should detect an existing version via legacy data and avoid inferring from setuptools_scm._integration import setuptools as setuptools_integration from setuptools_scm._integration.pyproject_reading import PyProjectData from setuptools_scm._integration.setup_cfg import SetuptoolsBasicData # Construct PyProjectData directly without requiring build backend inference pyproject_data = PyProjectData.for_testing( tool_name="setuptools_scm", is_required=False, # setuptools-scm not required section_present=False, # no [tool.setuptools_scm] section project_present=False, # no [project] section ) # Construct legacy data with version from setup.cfg legacy_data = SetuptoolsBasicData( path=tmp_path / "setup.cfg", name="legacy-proj", version="0.9.0" ) with pytest.warns(UserWarning, match="version of legacy-proj already set"): setuptools_integration.version_keyword( dist, "use_scm_version", True, _given_pyproject_data=pyproject_data, _given_legacy_data=legacy_data, ) # setuptools_scm should not set a version when setup.cfg already provided one assert dist.metadata.version is None def test_setuptools_version_keyword_ensures_regex( wd: WorkDir, monkeypatch: pytest.MonkeyPatch, ) -> None: wd.commit_testfile("test") wd("git tag 1.0") monkeypatch.chdir(wd.cwd) dist = create_clean_distribution("test") setuptools_integration.version_keyword( dist, "use_scm_version", {"tag_regex": "(1.0)"} ) assert dist.metadata.version == "1.0" @pytest.mark.parametrize( "ep_name", ["setuptools_scm.parse_scm", "setuptools_scm.parse_scm_fallback"] ) def test_git_archival_plugin_ignored(tmp_path: Path, ep_name: str) -> None: tmp_path.joinpath(".git_archival.txt").write_text("broken", encoding="utf-8") try: dist = importlib.metadata.distribution("setuptools_scm_git_archive") except importlib.metadata.PackageNotFoundError: pytest.skip("setuptools_scm_git_archive not installed") else: print(dist.metadata["Name"], dist.version) from setuptools_scm.discover import iter_matching_entrypoints found = list(iter_matching_entrypoints(tmp_path, config=c, entrypoint=ep_name)) imports = [item.value for item in found] assert "setuptools_scm_git_archive:parse" not in imports @pytest.mark.parametrize("base_name", ["setuptools_scm", "setuptools-scm"]) @pytest.mark.parametrize( "requirements", ["", ">=8", "[toml]>=7", "~=9.0", "[rich,toml]>=8"], ids=["empty", "version", "extras", "fuzzy", "multiple-extras"], ) def test_extract_package_name(base_name: str, requirements: str) -> None: """Test the _extract_package_name helper function""" assert extract_package_name(f"{base_name}{requirements}") == "setuptools-scm" # Helper function for creating and managing distribution objects def create_clean_distribution(name: str) -> setuptools.Distribution: """Create a clean distribution object without any setuptools_scm effects. This function creates a new setuptools Distribution and ensures it's completely clean from any previous setuptools_scm version inference effects, including: - Clearing any existing version - Removing the _setuptools_scm_version_set_by_infer flag """ import setuptools dist = setuptools.Distribution({"name": name}) # Clean all setuptools_scm effects dist.metadata.version = None if hasattr(dist, "_setuptools_scm_version_set_by_infer"): del dist._setuptools_scm_version_set_by_infer return dist def version_keyword_default( dist: setuptools.Distribution, pyproject_data: PyProjectData | None = None ) -> None: """Helper to call version_keyword with default config and return the result.""" setuptools_integration.version_keyword( dist, "use_scm_version", True, _given_pyproject_data=pyproject_data ) def version_keyword_calver( dist: setuptools.Distribution, pyproject_data: PyProjectData | None = None ) -> None: """Helper to call version_keyword with calver-by-date scheme and return the result.""" setuptools_integration.version_keyword( dist, "use_scm_version", {"version_scheme": "calver-by-date"}, _given_pyproject_data=pyproject_data, ) def infer_version_with_data( dist: setuptools.Distribution, pyproject_data: PyProjectData | None = None ) -> None: """Helper to call infer_version with pyproject data.""" setuptools_integration.infer_version(dist, _given_pyproject_data=pyproject_data) @pytest.mark.issue("https://github.com/pypa/setuptools_scm/issues/1022") @pytest.mark.filterwarnings("ignore:version of .* already set:UserWarning") @pytest.mark.filterwarnings( "ignore:.* does not correspond to a valid versioning date.*:UserWarning" ) @pytest.mark.parametrize( ("first_integration", "second_integration", "expected_final_version"), [ # infer_version and version_keyword can be called in either order (infer_version_with_data, version_keyword_default, "1.0.1.dev1"), (infer_version_with_data, version_keyword_calver, "9.2.13.0.dev1"), (version_keyword_default, infer_version_with_data, "1.0.1.dev1"), (version_keyword_calver, infer_version_with_data, "9.2.13.0.dev1"), ], ) def test_integration_function_call_order( wd: WorkDir, monkeypatch: pytest.MonkeyPatch, first_integration: Any, second_integration: Any, expected_final_version: str, ) -> None: """Test that integration functions can be called in any order. version_keyword should always win when it specifies configuration, but currently doesn't. Some tests will fail, showing the bug. """ # Set up controlled environment for deterministic versions monkeypatch.setenv("SOURCE_DATE_EPOCH", "1234567890") # 2009-02-13T23:31:30+00:00 # Override node_date to get consistent calver versions monkeypatch.setenv( "SETUPTOOLS_SCM_PRETEND_METADATA_FOR_TEST_CALL_ORDER", "{node_date=2009-02-13}" ) # Set up a git repository with a tag and known commit hash wd.commit_testfile("test") wd("git tag 1.0.0") wd.commit_testfile("test2") # Add another commit to get distance monkeypatch.chdir(wd.cwd) # Create PyProjectData with equivalent configuration - no file I/O! project_name = "test-call-order" pyproject_data = PyProjectData.for_testing( tool_name="setuptools_scm", project_name=project_name, has_dynamic_version=True, project_present=True, section_present=True, local_scheme="no-local-version", ) dist = create_clean_distribution(project_name) # Call both integration functions in order with direct data injection first_integration(dist, pyproject_data) second_integration(dist, pyproject_data) # Get the final version directly from the distribution final_version = dist.metadata.version # Assert the final version matches expectation # Some tests will fail here, demonstrating the bug where version_keyword doesn't override assert final_version == expected_final_version, ( f"Expected version '{expected_final_version}' but got '{final_version}'" ) @pytest.mark.issue("xmlsec-regression") @pytest.mark.skipif( sys.implementation.name == "pypy", reason="xmlsec build fails on PyPy in CI" ) def test_xmlsec_download_regression( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Test that pip download works for xmlsec package without causing setuptools_scm regression. This test ensures that downloading and building xmlsec from source doesn't fail due to setuptools_scm issues when using --no-build-isolation. """ # Set up environment with setuptools_scm debug enabled monkeypatch.setenv("SETUPTOOLS_SCM_DEBUG", "1") monkeypatch.setenv("COLUMNS", "150") # Run pip download command with no-binary and no-build-isolation try: subprocess.run( [ *(sys.executable, "-m", "pip", "download"), *("--no-binary", "xmlsec"), "--no-build-isolation", "-v", "xmlsec==1.3.16", ], cwd=tmp_path, timeout=300, check=True, ) except subprocess.CalledProcessError as e: pytest.fail(f"pip download failed: {e}", pytrace=False) # The success of the subprocess.run call above means the regression is fixed. # pip download succeeded without setuptools_scm causing version conflicts. @pytest.mark.issue(1252) def test_version_file_written_to_build_directory( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: """Test that version files are written to both source and build directory. Version files are written to source during inference (for development) and to the build directory during build_py (for wheel contents). """ monkeypatch.chdir(wd.cwd) wd.write( "pyproject.toml", textwrap.dedent("""\ [build-system] requires = ["setuptools>=61", "setuptools-scm"] build-backend = "setuptools.build_meta" [project] name = "test-pkg" dynamic = ["version"] [tool.setuptools_scm] version_file = "test_pkg/_version.py" [tool.setuptools.packages.find] where = ["."] """), ) pkg_dir = wd.cwd / "test_pkg" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text("from ._version import __version__\n") wd.commit_testfile() wd("git tag v1.0.0") _run_setuptools_setup(wd.cwd) version_file = pkg_dir / "_version.py" assert version_file.exists(), ( "Version file should be written to source tree during inference" ) assert "1.0.0" in version_file.read_text() # Build should also include version file in the wheel build_result = subprocess.run( [sys.executable, "-m", "build", "--wheel", "--no-isolation"], cwd=wd.cwd, capture_output=True, text=True, check=False, ) # Build should succeed assert build_result.returncode == 0, ( f"Build failed:\nstdout: {build_result.stdout}\nstderr: {build_result.stderr}" ) # Verify wheel was created and contains the version file dist_dir = wd.cwd / "dist" wheels = list(dist_dir.glob("*.whl")) assert len(wheels) == 1, f"Expected 1 wheel, found {len(wheels)}" # Check wheel contents for version file import zipfile with zipfile.ZipFile(wheels[0], "r") as whl: names = whl.namelist() version_file_in_wheel = any("_version.py" in name for name in names) assert version_file_in_wheel, ( f"Version file not found in wheel. Contents: {names}" ) @pytest.mark.issue(1252) def test_version_file_src_layout_path_transformation( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: """Test that version files work correctly with src/ layout. When version_file is specified as 'src/mypackage/_version.py', the path should be transformed to 'mypackage/_version.py' in the build directory. """ monkeypatch.chdir(wd.cwd) # Create a src/ layout project wd.write( "pyproject.toml", textwrap.dedent("""\ [build-system] requires = ["setuptools>=61", "setuptools-scm"] build-backend = "setuptools.build_meta" [project] name = "test-src-pkg" dynamic = ["version"] [tool.setuptools_scm] # Path includes src/ prefix as user would configure it version_file = "src/test_src_pkg/_version.py" [tool.setuptools.packages.find] where = ["src"] """), ) # Create src/ layout package structure src_dir = wd.cwd / "src" src_dir.mkdir() pkg_dir = src_dir / "test_src_pkg" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text("from ._version import __version__\n") # Commit so we have a valid git state wd.commit_testfile() wd("git tag v2.0.0") # Build wheel build_result = subprocess.run( [sys.executable, "-m", "build", "--wheel", "--no-isolation"], cwd=wd.cwd, capture_output=True, text=True, check=False, ) assert build_result.returncode == 0, ( f"Build failed:\nstdout: {build_result.stdout}\nstderr: {build_result.stderr}" ) # Verify wheel contains version file at correct path (without src/ prefix) import zipfile dist_dir = wd.cwd / "dist" wheels = list(dist_dir.glob("*.whl")) assert len(wheels) == 1 with zipfile.ZipFile(wheels[0], "r") as whl: names = whl.namelist() # Should be test_src_pkg/_version.py, NOT src/test_src_pkg/_version.py assert any(name == "test_src_pkg/_version.py" for name in names), ( f"Expected 'test_src_pkg/_version.py' in wheel, got: {names}" ) assert not any("src/" in name for name in names), ( f"Found 'src/' prefix in wheel paths: {names}" ) @pytest.mark.issue(1252) @pytest.mark.parametrize("editable_mode", ["compat", "strict"]) def test_editable_install_version_file( editable_mode: str, wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: """Test that editable installs write version files to source by default. Version files are written to the source tree during inference, making them importable for editable installs without any extra configuration. """ monkeypatch.chdir(wd.cwd) wd.write( "pyproject.toml", textwrap.dedent("""\ [build-system] requires = ["setuptools>=64", "setuptools-scm"] build-backend = "setuptools.build_meta" [project] name = "test-editable-pkg" dynamic = ["version"] [tool.setuptools_scm] version_file = "test_editable_pkg/_version.py" [tool.setuptools.packages.find] where = ["."] """), ) pkg_dir = wd.cwd / "test_editable_pkg" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text( "try:\n" " from ._version import __version__\n" "except ImportError:\n" " from importlib.metadata import version\n" " __version__ = version('test-editable-pkg')\n" ) wd.commit_testfile() wd("git tag v3.0.0") build_result = subprocess.run( [ sys.executable, "-m", "pip", "wheel", "--no-build-isolation", "-e", ".", "-w", "dist", f"--config-settings=editable_mode={editable_mode}", ], cwd=wd.cwd, capture_output=True, text=True, check=False, ) assert build_result.returncode == 0, ( f"Editable build ({editable_mode}) failed:\n" f"stdout: {build_result.stdout}\nstderr: {build_result.stderr}" ) version_file = pkg_dir / "_version.py" assert version_file.exists(), ( f"Version file should be written to source by default. Mode: {editable_mode}" ) content = version_file.read_text() assert "3.0.0" in content, f"Version file should contain '3.0.0': {content}" @pytest.mark.issue(1298) def test_editable_strict_includes_version_file( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: """Test that strict editable installs include version files in both locations. Version files are written to source during inference AND to the persistent auxiliary directory via build_py's get_outputs() registration. """ monkeypatch.chdir(wd.cwd) wd.write( "pyproject.toml", textwrap.dedent("""\ [build-system] requires = ["setuptools>=64", "setuptools-scm"] build-backend = "setuptools.build_meta" [project] name = "test-editable-strict-pkg" dynamic = ["version"] [tool.setuptools_scm] version_file = "test_editable_strict_pkg/_version.py" [tool.setuptools.packages.find] where = ["."] """), ) pkg_dir = wd.cwd / "test_editable_strict_pkg" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text( "try:\n" " from ._version import __version__\n" "except ImportError:\n" " from importlib.metadata import version\n" " __version__ = version('test-editable-strict-pkg')\n" ) wd.commit_testfile() wd("git tag v4.0.0") dist_dir = wd.cwd / "dist" dist_dir.mkdir(exist_ok=True) build_result = subprocess.run( [ sys.executable, "-c", ( "from setuptools.build_meta import build_editable; " "build_editable('dist', config_settings={'editable_mode': 'strict'})" ), ], cwd=wd.cwd, capture_output=True, text=True, check=False, ) assert build_result.returncode == 0, ( f"Strict editable build failed:\n" f"stdout: {build_result.stdout}\nstderr: {build_result.stderr}" ) # Version file should be in source (written during inference by default) version_file = pkg_dir / "_version.py" assert version_file.exists(), ( "Version file should be written to source tree during inference" ) assert "4.0.0" in version_file.read_text() # Version file should ALSO be in the persistent auxiliary directory build_dir = wd.cwd / "build" editable_dirs = list(build_dir.glob("__editable__.*")) assert editable_dirs, f"Expected editable build dir under {build_dir}" aux_version_file = editable_dirs[0] / "test_editable_strict_pkg" / "_version.py" assert aux_version_file.exists(), ( f"Version file should be in auxiliary dir for strict editable installs. " f"Checked: {aux_version_file}" ) assert "4.0.0" in aux_version_file.read_text() @pytest.mark.issue(1252) def test_readonly_source_directory_build( wd: WorkDir, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: """Test building from a read-only source directory succeeds. This is the core scenario from issue #1252: building a package when the source directory is read-only (e.g., Bazel builds). Version files should be written to the build directory, not the source tree. """ import stat monkeypatch.chdir(wd.cwd) # Create a minimal pyproject.toml with version_file configured wd.write( "pyproject.toml", textwrap.dedent("""\ [build-system] requires = ["setuptools>=61", "setuptools-scm"] build-backend = "setuptools.build_meta" [project] name = "readonly-test-pkg" dynamic = ["version"] [tool.setuptools_scm] version_file = "readonly_test_pkg/_version.py" [tool.setuptools.packages.find] where = ["."] """), ) # Create minimal package structure pkg_dir = wd.cwd / "readonly_test_pkg" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text("from ._version import __version__\n") # Commit so we have a valid git state wd.commit_testfile() wd("git tag v5.0.0") # Make the package directory read-only to simulate Bazel scenario original_mode = pkg_dir.stat().st_mode try: # Remove write permissions pkg_dir.chmod(stat.S_IRUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IROTH) # Verify we can't write to the directory. # On some Windows environments chmod() does not enforce directory # writability for newly created files, so skip if we cannot simulate # the read-only constraint reliably. version_file = pkg_dir / "_version.py" try: version_file.write_text("test") version_file.unlink(missing_ok=True) pytest.skip( "cannot enforce read-only directory semantics in this environment" ) except PermissionError: pass # Expected - directory is read-only # Disable writing to source since the directory is read-only. # build_py will still write version files to build_lib. build_result = subprocess.run( [sys.executable, "-m", "build", "--wheel", "--no-isolation"], cwd=wd.cwd, capture_output=True, text=True, check=False, env={**os.environ, "SETUPTOOLS_SCM_WRITE_TO_SOURCE": "0"}, ) # Build should succeed despite read-only source assert build_result.returncode == 0, ( f"Build from read-only source failed:\n" f"stdout: {build_result.stdout}\nstderr: {build_result.stderr}" ) # Verify wheel was created and contains the version file import zipfile dist_dir = wd.cwd / "dist" wheels = list(dist_dir.glob("*.whl")) assert len(wheels) == 1 with zipfile.ZipFile(wheels[0], "r") as whl: names = whl.namelist() assert any("_version.py" in name for name in names), ( f"Version file not found in wheel. Contents: {names}" ) # Read and verify version content version_content = None for name in names: if "_version.py" in name: version_content = whl.read(name).decode("utf-8") break assert version_content is not None assert "5.0.0" in version_content finally: # Restore original permissions for cleanup pkg_dir.chmod(original_mode) @pytest.mark.issue(1252) def test_legacy_write_to_build_directory( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: """Test that legacy write_to configuration also writes to build directory. The write_to option is the older way to configure version file writing. It should also be deferred to build_py just like version_file. """ monkeypatch.chdir(wd.cwd) # Create a pyproject.toml using legacy write_to wd.write( "pyproject.toml", textwrap.dedent("""\ [build-system] requires = ["setuptools>=61", "setuptools-scm"] build-backend = "setuptools.build_meta" [project] name = "legacy-write-to-pkg" dynamic = ["version"] [tool.setuptools_scm] write_to = "legacy_pkg/_version.py" [tool.setuptools.packages.find] where = ["."] """), ) # Create minimal package structure pkg_dir = wd.cwd / "legacy_pkg" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text("from ._version import __version__\n") # Commit so we have a valid git state wd.commit_testfile() wd("git tag v6.0.0") _run_setuptools_setup(wd.cwd) version_file = pkg_dir / "_version.py" assert version_file.exists(), ( "Legacy write_to should write to source during inference" ) assert "6.0.0" in version_file.read_text() # Build wheel should also include it build_result = subprocess.run( [sys.executable, "-m", "build", "--wheel", "--no-isolation"], cwd=wd.cwd, capture_output=True, text=True, check=False, ) assert build_result.returncode == 0, ( f"Build failed:\nstdout: {build_result.stdout}\nstderr: {build_result.stderr}" ) # Verify wheel contains version file import zipfile dist_dir = wd.cwd / "dist" wheels = list(dist_dir.glob("*.whl")) assert len(wheels) == 1 with zipfile.ZipFile(wheels[0], "r") as whl: names = whl.namelist() assert any("_version.py" in name for name in names), ( f"Version file not found in wheel. Contents: {names}" ) @pytest.mark.issue(1252) def test_version_file_template_in_build( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: """Test that custom version_file_template works during build_py. Verifies that the template is correctly applied when writing the version file to the build directory. """ monkeypatch.chdir(wd.cwd) # Create a pyproject.toml with custom template wd.write( "pyproject.toml", textwrap.dedent("""\ [build-system] requires = ["setuptools>=61", "setuptools-scm"] build-backend = "setuptools.build_meta" [project] name = "template-test-pkg" dynamic = ["version"] [tool.setuptools_scm] version_file = "template_pkg/_version.py" version_file_template = ''' # Custom template for testing __version__ = "{version}" VERSION_TUPLE = {version_tuple} CUSTOM_MARKER = "build_py_template_test" ''' [tool.setuptools.packages.find] where = ["."] """), ) # Create minimal package structure pkg_dir = wd.cwd / "template_pkg" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text("from ._version import __version__\n") # Commit so we have a valid git state wd.commit_testfile() wd("git tag v7.1.2") # Build wheel build_result = subprocess.run( [sys.executable, "-m", "build", "--wheel", "--no-isolation"], cwd=wd.cwd, capture_output=True, text=True, check=False, ) assert build_result.returncode == 0, ( f"Build failed:\nstdout: {build_result.stdout}\nstderr: {build_result.stderr}" ) # Verify wheel contains version file with custom template content import zipfile dist_dir = wd.cwd / "dist" wheels = list(dist_dir.glob("*.whl")) assert len(wheels) == 1 with zipfile.ZipFile(wheels[0], "r") as whl: names = whl.namelist() # Find and read version file version_content = None for name in names: if "_version.py" in name: version_content = whl.read(name).decode("utf-8") break assert version_content is not None, f"Version file not found in {names}" assert "CUSTOM_MARKER" in version_content, ( f"Custom template marker not found. Content:\n{version_content}" ) assert "build_py_template_test" in version_content assert "7.1.2" in version_content assert "VERSION_TUPLE" in version_content @pytest.mark.issue(1252) def test_custom_build_py_still_writes_version_file( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: """Custom build_py should not disable setuptools-scm version file writing.""" monkeypatch.chdir(wd.cwd) wd.write( "pyproject.toml", textwrap.dedent("""\ [build-system] requires = ["setuptools>=61", "setuptools-scm"] build-backend = "setuptools.build_meta" """), ) wd.write( "setup.py", textwrap.dedent("""\ from pathlib import Path from setuptools import setup from setuptools.command.build_py import build_py as _build_py class CustomBuildPy(_build_py): def run(self): super().run() Path("custom_build_py_ran.txt").write_text("ran", encoding="utf-8") setup( name="custom-build-py-pkg", packages=["custom_build_py_pkg"], use_scm_version={"version_file": "custom_build_py_pkg/_version.py"}, cmdclass={"build_py": CustomBuildPy}, ) """), ) pkg_dir = wd.cwd / "custom_build_py_pkg" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text("from ._version import __version__\n") wd.commit_testfile() wd("git tag v8.0.0") build_result = subprocess.run( [sys.executable, "-m", "build", "--wheel", "--no-isolation"], cwd=wd.cwd, capture_output=True, text=True, check=False, ) assert build_result.returncode == 0, ( f"Build failed:\nstdout: {build_result.stdout}\nstderr: {build_result.stderr}" ) assert (wd.cwd / "custom_build_py_ran.txt").exists(), ( "Project custom build_py should still run" ) import zipfile dist_dir = wd.cwd / "dist" wheels = list(dist_dir.glob("*.whl")) assert len(wheels) == 1 with zipfile.ZipFile(wheels[0], "r") as whl: names = whl.namelist() assert "custom_build_py_pkg/_version.py" in names, ( f"Expected version file in wheel, got: {names}" ) @pytest.mark.issue(1298) def test_version_file_contains_commit_node_in_wheel( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: """Test that the commit node (hash) is present in the version file inside a wheel. The default .py template writes commit_id = {scm_version.short_node!r}. This verifies the full pipeline: git commit -> ScmVersion.node -> version file in the built wheel. """ monkeypatch.chdir(wd.cwd) wd.write( "pyproject.toml", textwrap.dedent("""\ [build-system] requires = ["setuptools>=64", "setuptools-scm"] build-backend = "setuptools.build_meta" [project] name = "node-test-pkg" dynamic = ["version"] [tool.setuptools_scm] version_file = "node_test_pkg/_version.py" [tool.setuptools.packages.find] where = ["."] """), ) pkg_dir = wd.cwd / "node_test_pkg" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text( "from ._version import __version__, commit_id\n" ) wd.commit_testfile() wd("git tag v1.0.0") # Get the commit hash so we can verify it in the version file git_node = wd("git rev-parse HEAD").strip() short_node = git_node[:7] build_result = subprocess.run( [sys.executable, "-m", "build", "--wheel", "--no-isolation"], cwd=wd.cwd, capture_output=True, text=True, check=False, ) assert build_result.returncode == 0, ( f"Build failed:\nstdout: {build_result.stdout}\nstderr: {build_result.stderr}" ) import zipfile dist_dir = wd.cwd / "dist" wheels = list(dist_dir.glob("*.whl")) assert len(wheels) == 1 with zipfile.ZipFile(wheels[0], "r") as whl: version_content = None for name in whl.namelist(): if "_version.py" in name: version_content = whl.read(name).decode("utf-8") break assert version_content is not None, "Version file not found in wheel" assert "1.0.0" in version_content assert "commit_id" in version_content assert short_node in version_content, ( f"Expected short node '{short_node}' in version file. " f"Content:\n{version_content}" ) @pytest.mark.issue(1298) def test_version_file_contains_commit_node_in_editable_strict( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: """Test that the commit node is present in the version file for strict editable installs. In strict mode, version files should be copied to the persistent auxiliary directory with the full commit_id included. """ monkeypatch.chdir(wd.cwd) wd.write( "pyproject.toml", textwrap.dedent("""\ [build-system] requires = ["setuptools>=64", "setuptools-scm"] build-backend = "setuptools.build_meta" [project] name = "node-editable-pkg" dynamic = ["version"] [tool.setuptools_scm] version_file = "node_editable_pkg/_version.py" [tool.setuptools.packages.find] where = ["."] """), ) pkg_dir = wd.cwd / "node_editable_pkg" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text( "from ._version import __version__, commit_id\n" ) wd.commit_testfile() wd("git tag v2.0.0") git_node = wd("git rev-parse HEAD").strip() short_node = git_node[:7] dist_dir = wd.cwd / "dist" dist_dir.mkdir(exist_ok=True) build_result = subprocess.run( [ sys.executable, "-c", ( "from setuptools.build_meta import build_editable; " "build_editable('dist', config_settings={'editable_mode': 'strict'})" ), ], cwd=wd.cwd, capture_output=True, text=True, check=False, ) assert build_result.returncode == 0, ( f"Strict editable build failed:\n" f"stdout: {build_result.stdout}\nstderr: {build_result.stderr}" ) # Version file should be in source (written during inference) source_version_file = pkg_dir / "_version.py" assert source_version_file.exists() content = source_version_file.read_text() assert "2.0.0" in content assert "commit_id" in content assert short_node in content, ( f"Expected short node '{short_node}' in source version file. Content:\n{content}" ) # Version file should ALSO be in the auxiliary directory build_dir = wd.cwd / "build" editable_dirs = list(build_dir.glob("__editable__.*")) assert editable_dirs, f"Expected editable build dir under {build_dir}" aux_version_file = editable_dirs[0] / "node_editable_pkg" / "_version.py" assert aux_version_file.exists(), ( f"Version file missing from auxiliary dir: {aux_version_file}" ) aux_content = aux_version_file.read_text() assert short_node in aux_content def _sdist_names(wd: WorkDir) -> list[str]: """Build an sdist in *wd* and return the list of archive member names.""" import shutil import tarfile dist_dir = wd.cwd / "dist" if dist_dir.exists(): shutil.rmtree(dist_dir) egg_info_dirs = list(wd.cwd.glob("*.egg-info")) for d in egg_info_dirs: shutil.rmtree(d) build_result = subprocess.run( [sys.executable, "-m", "build", "--sdist", "--no-isolation"], cwd=wd.cwd, capture_output=True, text=True, check=False, ) assert build_result.returncode == 0, ( f"sdist build failed:\nstdout: {build_result.stdout}\n" f"stderr: {build_result.stderr}" ) sdists = list(dist_dir.glob("*.tar.gz")) assert len(sdists) == 1, f"Expected 1 sdist, found {len(sdists)}" with tarfile.open(sdists[0], "r:gz") as tar: return tar.getnames() def test_manifest_in_excludes_scm_tracked_files( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: """MANIFEST.in exclusions must be honoured even when the egg_info mixin supplies tracked files directly (bypassing walk_revctrl). First verifies that without the exclusion the file IS included, then adds ``exclude secret.txt`` and verifies the file is gone. """ monkeypatch.chdir(wd.cwd) wd.write( "pyproject.toml", textwrap.dedent("""\ [build-system] requires = ["setuptools>=61", "setuptools-scm"] build-backend = "setuptools.build_meta" [project] name = "test-pkg" dynamic = ["version"] [tool.setuptools_scm] """), ) pkg_dir = wd.cwd / "test_pkg" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text("") wd.write("secret.txt", "do-not-ship") # Empty MANIFEST.in -- no exclusions wd.write("MANIFEST.in", "") wd(wd.add_command) wd.commit() wd("git tag v1.0.0") # -- Phase 1: empty MANIFEST.in → file is included ---------------------- names = _sdist_names(wd) assert any("secret.txt" in n for n in names), ( f"secret.txt should be included when MANIFEST.in is empty: {names}" ) assert any("test_pkg/__init__.py" in n for n in names), ( f"test_pkg/__init__.py should be in sdist: {names}" ) # -- Phase 2: MANIFEST.in excludes the file → gone ---------------------- wd.write("MANIFEST.in", "exclude secret.txt\n") wd(wd.add_command) wd.commit() names = _sdist_names(wd) assert not any("secret.txt" in n for n in names), ( f"secret.txt should have been excluded by MANIFEST.in: {names}" ) assert any("test_pkg/__init__.py" in n for n in names), ( f"test_pkg/__init__.py should still be in sdist: {names}" ) @pytest.mark.issue(1473) def test_scm_metadata_in_sdist_not_in_wheel( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: """SCM egg-info JSON is for sdist fallback; wheels must not ship it.""" import shutil import zipfile from vcs_versioning._scm_metadata import SCM_FILE_LIST_FILENAME from vcs_versioning._scm_metadata import SCM_VERSION_FILENAME monkeypatch.chdir(wd.cwd) wd.write( "pyproject.toml", textwrap.dedent("""\ [build-system] requires = ["setuptools>=61", "setuptools-scm"] build-backend = "setuptools.build_meta" [project] name = "test-pkg" dynamic = ["version"] [tool.setuptools_scm] """), ) pkg_dir = wd.cwd / "test_pkg" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text("") wd(wd.add_command) wd.commit() wd("git tag v1.0.0") sdist_names = _sdist_names(wd) assert any(SCM_VERSION_FILENAME in n for n in sdist_names), ( f"{SCM_VERSION_FILENAME} should be in sdist: {sdist_names}" ) assert any(SCM_FILE_LIST_FILENAME in n for n in sdist_names), ( f"{SCM_FILE_LIST_FILENAME} should be in sdist: {sdist_names}" ) dist_dir = wd.cwd / "dist" if dist_dir.exists(): shutil.rmtree(dist_dir) for egg_info_dir in wd.cwd.glob("*.egg-info"): shutil.rmtree(egg_info_dir) build_result = subprocess.run( [sys.executable, "-m", "build", "--wheel", "--no-isolation"], cwd=wd.cwd, capture_output=True, text=True, check=False, ) assert build_result.returncode == 0, ( f"wheel build failed:\nstdout: {build_result.stdout}\n" f"stderr: {build_result.stderr}" ) wheels = list(dist_dir.glob("*.whl")) assert len(wheels) == 1, f"Expected 1 wheel, found {len(wheels)}" with zipfile.ZipFile(wheels[0], "r") as whl: names = whl.namelist() assert not any(SCM_VERSION_FILENAME in n for n in names), ( f"{SCM_VERSION_FILENAME} must not be in wheel: {names}" ) assert not any(SCM_FILE_LIST_FILENAME in n for n in names), ( f"{SCM_FILE_LIST_FILENAME} must not be in wheel: {names}" ) class TestIsInsidePackage: """Unit tests for _is_inside_package.""" def test_root_level_file_not_inside_package(self) -> None: from setuptools_scm._integration.build_py import _is_inside_package assert not _is_inside_package("VERSION", ["mypackage"]) def test_root_level_py_file_not_inside_package(self) -> None: from setuptools_scm._integration.build_py import _is_inside_package assert not _is_inside_package("_version.py", ["mypackage"]) def test_file_inside_package(self) -> None: from setuptools_scm._integration.build_py import _is_inside_package assert _is_inside_package("mypackage/_version.py", ["mypackage"]) def test_file_inside_nested_package(self) -> None: from setuptools_scm._integration.build_py import _is_inside_package assert _is_inside_package("mypackage/sub/_version.py", ["mypackage"]) def test_file_inside_dotted_package(self) -> None: from setuptools_scm._integration.build_py import _is_inside_package assert _is_inside_package("mypackage/sub/_version.py", ["mypackage.sub"]) def test_file_not_in_any_package(self) -> None: from setuptools_scm._integration.build_py import _is_inside_package assert not _is_inside_package("other/version.h", ["mypackage"]) def test_no_packages(self) -> None: from setuptools_scm._integration.build_py import _is_inside_package assert not _is_inside_package("mypackage/_version.py", None) assert not _is_inside_package("mypackage/_version.py", []) @pytest.mark.skipif(os.name != "posix", reason="posix absolute path form") def test_rejects_absolute_path(self) -> None: from setuptools_scm._integration.build_py import _is_inside_package with pytest.raises(ValueError, match="must be relative"): _is_inside_package("/etc/passwd", ["mypackage"]) @pytest.mark.skipif(os.name != "nt", reason="windows absolute path form") def test_rejects_absolute_path_windows(self) -> None: from setuptools_scm._integration.build_py import _is_inside_package with pytest.raises(ValueError, match="must be relative"): _is_inside_package("C:\\Users\\evil.py", ["mypackage"]) def test_rejects_traversal(self) -> None: from setuptools_scm._integration.build_py import _is_inside_package with pytest.raises(ValueError, match="must not contain"): _is_inside_package("mypackage/../../etc/passwd", ["mypackage"]) class TestSanitizeRelativePath: """Unit tests for _sanitize_relative_path.""" def test_accepts_simple_relative(self) -> None: from setuptools_scm._integration.build_py import _sanitize_relative_path result = _sanitize_relative_path("mypackage/_version.py") assert result == Path("mypackage/_version.py") @pytest.mark.skipif(os.name != "posix", reason="posix absolute path form") def test_rejects_absolute_posix(self) -> None: from setuptools_scm._integration.build_py import _sanitize_relative_path with pytest.raises(ValueError, match="must be relative"): _sanitize_relative_path("/tmp/evil.py") @pytest.mark.skipif(os.name != "nt", reason="windows absolute path form") def test_rejects_absolute_windows(self) -> None: from setuptools_scm._integration.build_py import _sanitize_relative_path with pytest.raises(ValueError, match="must be relative"): _sanitize_relative_path("C:\\Windows\\evil.py") def test_rejects_dotdot(self) -> None: from setuptools_scm._integration.build_py import _sanitize_relative_path with pytest.raises(ValueError, match="must not contain"): _sanitize_relative_path("pkg/../../outside.py") def test_accepts_root_level(self) -> None: from setuptools_scm._integration.build_py import _sanitize_relative_path result = _sanitize_relative_path("VERSION.txt") assert result == Path("VERSION.txt") @pytest.mark.issue(1364) def test_root_level_version_file_not_in_wheel( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: """Root-level version files (e.g. VERSION) must not end up in the wheel. Regression test for GH-1364: upgrading to setuptools-scm 10 caused files like ``VERSION`` to appear at the wheel root because build_py unconditionally wrote them to ``build_lib``. """ monkeypatch.chdir(wd.cwd) wd.write( "pyproject.toml", textwrap.dedent("""\ [build-system] requires = ["setuptools>=61", "setuptools-scm"] build-backend = "setuptools.build_meta" [project] name = "root-version-file-pkg" dynamic = ["version"] [tool.setuptools_scm] write_to = "VERSION.txt" [tool.setuptools.packages.find] where = ["."] """), ) pkg_dir = wd.cwd / "root_version_file_pkg" pkg_dir.mkdir() (pkg_dir / "__init__.py").write_text("") wd.commit_testfile() wd("git tag v1.0.0") build_result = subprocess.run( [sys.executable, "-m", "build", "--wheel", "--no-isolation"], cwd=wd.cwd, capture_output=True, text=True, check=False, ) assert build_result.returncode == 0, ( f"Build failed:\nstdout: {build_result.stdout}\nstderr: {build_result.stderr}" ) import zipfile dist_dir = wd.cwd / "dist" wheels = list(dist_dir.glob("*.whl")) assert len(wheels) == 1 with zipfile.ZipFile(wheels[0], "r") as whl: names = whl.namelist() assert not any(n == "VERSION.txt" for n in names), ( f"Root-level VERSION.txt should NOT be in wheel, got: {names}" )