diff --git a/CHANGELOG.md b/CHANGELOG.md index caf73ad3..ed1a923e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ ### changed -- refine activation logic and add unittest for the relevant cases instead of trying to speedrun setuptools +- refine activation logic and add unittest for the relevant cases instead of trying to speedrun setuptools ## v9.1.1 diff --git a/src/setuptools_scm/_cli.py b/src/setuptools_scm/_cli.py index 1f104f46..6ed4a2e5 100644 --- a/src/setuptools_scm/_cli.py +++ b/src/setuptools_scm/_cli.py @@ -11,10 +11,13 @@ from setuptools_scm import Configuration from setuptools_scm._file_finders import find_files from setuptools_scm._get_version_impl import _get_version +from setuptools_scm._integration.pyproject_reading import PyProjectData from setuptools_scm.discover import walk_potential_roots -def main(args: list[str] | None = None) -> int: +def main( + args: list[str] | None = None, *, _given_pyproject_data: PyProjectData | None = None +) -> int: opts = _get_cli_opts(args) inferred_root: str = opts.root or "." @@ -24,6 +27,7 @@ def main(args: list[str] | None = None) -> int: config = Configuration.from_file( pyproject, root=(os.path.abspath(opts.root) if opts.root is not None else None), + pyproject_data=_given_pyproject_data, ) except (LookupError, FileNotFoundError) as ex: # no pyproject.toml OR no [tool.setuptools_scm] diff --git a/testing/conftest.py b/testing/conftest.py index de1d9900..8b12f778 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -22,7 +22,7 @@ from .wd_wrapper import WorkDir -def pytest_configure() -> None: +def pytest_configure(config: pytest.Config) -> None: # 2009-02-13T23:31:30+00:00 os.environ["SOURCE_DATE_EPOCH"] = "1234567890" os.environ["SETUPTOOLS_SCM_DEBUG"] = "1" @@ -42,10 +42,10 @@ def pytest_report_header() -> list[str]: # Replace everything up to and including site-packages with site:: parts = path.split("site-packages", 1) if len(parts) > 1: - path = "site:." + parts[1] + path = "site::" + parts[1] elif path and str(Path.cwd()) in path: # Replace current working directory with CWD:: - path = path.replace(str(Path.cwd()), "CWD:.") + path = path.replace(str(Path.cwd()), "CWD::") res.append(f"{pkg} version {pkg_version} from {path}") return res @@ -88,8 +88,28 @@ def debug_mode() -> Iterator[DebugMode]: yield debug_mode +def setup_git_wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch | None = None) -> WorkDir: + """Set up a WorkDir with git initialized and configured for testing. + + Note: This is a compatibility wrapper. Consider using wd.setup_git() directly. + """ + return wd.setup_git(monkeypatch) + + +def setup_hg_wd(wd: WorkDir) -> WorkDir: + """Set up a WorkDir with mercurial initialized and configured for testing. + + Note: This is a compatibility wrapper. Consider using wd.setup_hg() directly. + """ + return wd.setup_hg() + + @pytest.fixture def wd(tmp_path: Path) -> WorkDir: + """Base WorkDir fixture that returns an unconfigured working directory. + + Individual test modules should override this fixture to set up specific SCM configurations. + """ target_wd = tmp_path.resolve() / "wd" target_wd.mkdir() return WorkDir(target_wd) diff --git a/testing/test_better_root_errors.py b/testing/test_better_root_errors.py index 0ba964cc..a0c19949 100644 --- a/testing/test_better_root_errors.py +++ b/testing/test_better_root_errors.py @@ -16,32 +16,13 @@ from setuptools_scm._get_version_impl import _version_missing from testing.wd_wrapper import WorkDir - -def setup_git_repo(wd: WorkDir) -> WorkDir: - """Set up a git repository for testing.""" - wd("git init") - wd("git config user.email test@example.com") - wd('git config user.name "a test"') - wd.add_command = "git add ." - wd.commit_command = "git commit -m test-{reason}" - return wd - - -def setup_hg_repo(wd: WorkDir) -> WorkDir: - """Set up a mercurial repository for testing.""" - try: - wd("hg init") - wd.add_command = "hg add ." - wd.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"' - return wd - except Exception: - pytest.skip("hg not available") +# No longer need to import setup functions - using WorkDir methods directly def test_find_scm_in_parents_finds_git(wd: WorkDir) -> None: """Test that _find_scm_in_parents correctly finds git repositories in parent directories.""" # Set up git repo in root - setup_git_repo(wd) + wd.setup_git() # Create a subdirectory structure subdir = wd.cwd / "subproject" / "nested" @@ -57,7 +38,7 @@ def test_find_scm_in_parents_finds_git(wd: WorkDir) -> None: def test_find_scm_in_parents_finds_hg(wd: WorkDir) -> None: """Test that _find_scm_in_parents correctly finds mercurial repositories in parent directories.""" # Set up hg repo in root - setup_hg_repo(wd) + wd.setup_hg() # Create a subdirectory structure subdir = wd.cwd / "subproject" / "nested" @@ -85,7 +66,7 @@ def test_find_scm_in_parents_returns_none(wd: WorkDir) -> None: def test_version_missing_with_scm_in_parent(wd: WorkDir) -> None: """Test that _version_missing provides helpful error message when SCM is found in parent.""" # Set up git repo in root - setup_git_repo(wd) + wd.setup_git() # Create a subdirectory structure subdir = wd.cwd / "subproject" / "nested" @@ -130,7 +111,7 @@ def test_version_missing_no_scm_found(wd: WorkDir) -> None: def test_version_missing_with_relative_to_set(wd: WorkDir) -> None: """Test that when relative_to is set, we don't search parents for error messages.""" # Set up git repo in root - setup_git_repo(wd) + wd.setup_git() # Create a subdirectory structure subdir = wd.cwd / "subproject" / "nested" @@ -161,7 +142,7 @@ def test_search_parent_directories_works_as_suggested( ) -> None: """Test that the suggested search_parent_directories=True solution actually works.""" # Set up git repo - setup_git_repo(wd) + wd.setup_git() wd.commit_testfile() # Make sure there's a commit for version detection # Create a subdirectory @@ -182,7 +163,7 @@ def test_integration_better_error_from_nested_directory( ) -> None: """Integration test: get_version from nested directory should give helpful error.""" # Set up git repo - setup_git_repo(wd) + wd.setup_git() # Create a subdirectory subdir = wd.cwd / "subproject" diff --git a/testing/test_cli.py b/testing/test_cli.py index ffdcebd2..46a0f3aa 100644 --- a/testing/test_cli.py +++ b/testing/test_cli.py @@ -7,19 +7,46 @@ import pytest from setuptools_scm._cli import main +from setuptools_scm._integration.pyproject_reading import PyProjectData from .conftest import DebugMode -from .test_git import wd as wd_fixture # noqa: F401 (evil fixture reuse) from .wd_wrapper import WorkDir + +@pytest.fixture +def wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch, debug_mode: DebugMode) -> WorkDir: + """Set up git for CLI tests.""" + debug_mode.disable() + wd.setup_git(monkeypatch) + debug_mode.enable() + return wd + + PYPROJECT_TOML = "pyproject.toml" PYPROJECT_SIMPLE = "[tool.setuptools_scm]" PYPROJECT_ROOT = '[tool.setuptools_scm]\nroot=".."' +# PyProjectData constants for testing +PYPROJECT_DATA_SIMPLE = PyProjectData.for_testing(section_present=True) +PYPROJECT_DATA_WITH_PROJECT = PyProjectData.for_testing( + section_present=True, project_present=True, project_name="test" +) + -def get_output(args: list[str]) -> str: +def _create_version_file_pyproject_data() -> PyProjectData: + """Create PyProjectData with version_file configuration for testing.""" + data = PyProjectData.for_testing( + section_present=True, project_present=True, project_name="test" + ) + data.section["version_file"] = "ver.py" + return data + + +def get_output( + args: list[str], *, _given_pyproject_data: PyProjectData | None = None +) -> str: with redirect_stdout(io.StringIO()) as out: - main(args) + main(args, _given_pyproject_data=_given_pyproject_data) return out.getvalue() @@ -59,24 +86,20 @@ def test_cli_force_version_files( ) -> None: debug_mode.disable() wd.commit_testfile() - wd.write( - PYPROJECT_TOML, - """ -[project] -name = "test" -[tool.setuptools_scm] -version_file = "ver.py" -""", - ) monkeypatch.chdir(wd.cwd) version_file = wd.cwd.joinpath("ver.py") assert not version_file.exists() - get_output([]) + # Create pyproject data with version_file configuration + pyproject_data = _create_version_file_pyproject_data() + + get_output([], _given_pyproject_data=pyproject_data) assert not version_file.exists() - output = get_output(["--force-write-version-files"]) + output = get_output( + ["--force-write-version-files"], _given_pyproject_data=pyproject_data + ) assert version_file.exists() assert output[:5] in version_file.read_text("utf-8") @@ -87,13 +110,16 @@ def test_cli_create_archival_file_stable( ) -> None: """Test creating stable .git_archival.txt file.""" wd.commit_testfile() - wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) monkeypatch.chdir(wd.cwd) archival_file = wd.cwd / ".git_archival.txt" assert not archival_file.exists() - result = main(["create-archival-file", "--stable"]) + # Use injected pyproject data instead of creating a file + pyproject_data = PYPROJECT_DATA_SIMPLE + result = main( + ["create-archival-file", "--stable"], _given_pyproject_data=pyproject_data + ) assert result == 0 assert archival_file.exists() @@ -115,13 +141,16 @@ def test_cli_create_archival_file_full( ) -> None: """Test creating full .git_archival.txt file with branch information.""" wd.commit_testfile() - wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) monkeypatch.chdir(wd.cwd) archival_file = wd.cwd / ".git_archival.txt" assert not archival_file.exists() - result = main(["create-archival-file", "--full"]) + # Use injected pyproject data instead of creating a file + pyproject_data = PYPROJECT_DATA_SIMPLE + result = main( + ["create-archival-file", "--full"], _given_pyproject_data=pyproject_data + ) assert result == 0 assert archival_file.exists() @@ -144,15 +173,18 @@ def test_cli_create_archival_file_exists_no_force( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: """Test that existing .git_archival.txt file prevents creation without --force.""" + wd.setup_git(monkeypatch) wd.commit_testfile() - wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) monkeypatch.chdir(wd.cwd) archival_file = wd.cwd / ".git_archival.txt" archival_file.write_text("existing content", encoding="utf-8") # Should fail without --force - result = main(["create-archival-file", "--stable"]) + pyproject_data = PYPROJECT_DATA_SIMPLE + result = main( + ["create-archival-file", "--stable"], _given_pyproject_data=pyproject_data + ) assert result == 1 # Content should be unchanged @@ -163,15 +195,19 @@ def test_cli_create_archival_file_exists_with_force( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: """Test that --force overwrites existing .git_archival.txt file.""" + wd.setup_git(monkeypatch) wd.commit_testfile() - wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) monkeypatch.chdir(wd.cwd) archival_file = wd.cwd / ".git_archival.txt" archival_file.write_text("existing content", encoding="utf-8") # Should succeed with --force - result = main(["create-archival-file", "--stable", "--force"]) + pyproject_data = PYPROJECT_DATA_SIMPLE + result = main( + ["create-archival-file", "--stable", "--force"], + _given_pyproject_data=pyproject_data, + ) assert result == 0 # Content should be updated @@ -184,26 +220,31 @@ def test_cli_create_archival_file_requires_stable_or_full( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: """Test that create-archival-file requires either --stable or --full.""" + wd.setup_git(monkeypatch) wd.commit_testfile() - wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) monkeypatch.chdir(wd.cwd) # Should fail without --stable or --full + pyproject_data = PYPROJECT_DATA_SIMPLE with pytest.raises(SystemExit): - main(["create-archival-file"]) + main(["create-archival-file"], _given_pyproject_data=pyproject_data) def test_cli_create_archival_file_mutually_exclusive( wd: WorkDir, monkeypatch: pytest.MonkeyPatch ) -> None: """Test that --stable and --full are mutually exclusive.""" + wd.setup_git(monkeypatch) wd.commit_testfile() - wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) monkeypatch.chdir(wd.cwd) # Should fail with both --stable and --full + pyproject_data = PYPROJECT_DATA_SIMPLE with pytest.raises(SystemExit): - main(["create-archival-file", "--stable", "--full"]) + main( + ["create-archival-file", "--stable", "--full"], + _given_pyproject_data=pyproject_data, + ) def test_cli_create_archival_file_existing_gitattributes( @@ -211,14 +252,16 @@ def test_cli_create_archival_file_existing_gitattributes( ) -> None: """Test behavior when .gitattributes already has export-subst configuration.""" wd.commit_testfile() - wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) monkeypatch.chdir(wd.cwd) # Create .gitattributes with export-subst configuration gitattributes_file = wd.cwd / ".gitattributes" gitattributes_file.write_text(".git_archival.txt export-subst\n", encoding="utf-8") - result = main(["create-archival-file", "--stable"]) + pyproject_data = PYPROJECT_DATA_SIMPLE + result = main( + ["create-archival-file", "--stable"], _given_pyproject_data=pyproject_data + ) assert result == 0 archival_file = wd.cwd / ".git_archival.txt" @@ -230,10 +273,12 @@ def test_cli_create_archival_file_no_gitattributes( ) -> None: """Test behavior when .gitattributes doesn't exist or lacks export-subst.""" wd.commit_testfile() - wd.write(PYPROJECT_TOML, PYPROJECT_SIMPLE) monkeypatch.chdir(wd.cwd) - result = main(["create-archival-file", "--stable"]) + pyproject_data = PYPROJECT_DATA_SIMPLE + result = main( + ["create-archival-file", "--stable"], _given_pyproject_data=pyproject_data + ) assert result == 0 archival_file = wd.cwd / ".git_archival.txt" diff --git a/testing/test_file_finder.py b/testing/test_file_finder.py index 7d31e7d1..22a10f81 100644 --- a/testing/test_file_finder.py +++ b/testing/test_file_finder.py @@ -18,21 +18,9 @@ def inwd( ) -> WorkDir: param: str = request.param # type: ignore[attr-defined] if param == "git": - try: - wd("git init") - except OSError: - pytest.skip("git executable not found") - wd("git config user.email test@example.com") - wd('git config user.name "a test"') - wd.add_command = "git add ." - wd.commit_command = "git commit -m test-{reason}" + wd.setup_git(monkeypatch) elif param == "hg": - try: - wd("hg init") - except OSError: - pytest.skip("hg executable not found") - wd.add_command = "hg add ." - wd.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"' + wd.setup_hg() (wd.cwd / "file1").touch() adir = wd.cwd / "adir" adir.mkdir() @@ -249,10 +237,7 @@ def test_archive( @pytest.fixture def hg_wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> WorkDir: - try: - wd("hg init") - except OSError: - pytest.skip("hg executable not found") + wd.setup_hg() (wd.cwd / "file").touch() wd("hg add file") monkeypatch.chdir(wd.cwd) diff --git a/testing/test_git.py b/testing/test_git.py index 31cac7a3..0ef39458 100644 --- a/testing/test_git.py +++ b/testing/test_git.py @@ -34,27 +34,14 @@ from .conftest import DebugMode from .wd_wrapper import WorkDir -pytestmark = pytest.mark.skipif( - not has_command("git", warn=False), reason="git executable not found" -) - - -def setup_git_wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch | None = None) -> WorkDir: - """Set up a WorkDir with git initialized and configured for testing.""" - if monkeypatch: - monkeypatch.delenv("HOME", raising=False) - wd("git init") - wd("git config user.email test@example.com") - wd('git config user.name "a test"') - wd.add_command = "git add ." - wd.commit_command = "git commit -m test-{reason}" - return wd +# Note: Git availability is now checked in WorkDir.setup_git() method -@pytest.fixture(name="wd") +@pytest.fixture def wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch, debug_mode: DebugMode) -> WorkDir: + """Set up git for git-specific tests.""" debug_mode.disable() - setup_git_wd(wd, monkeypatch) + wd.setup_git(monkeypatch) debug_mode.enable() return wd @@ -685,7 +672,7 @@ def test_fail_on_missing_submodules_with_uninitialized_submodules( # Create a test repository with a .gitmodules file but no actual submodule test_repo = tmp_path / "test_repo" test_repo.mkdir() - test_wd = setup_git_wd(WorkDir(test_repo)) + test_wd = WorkDir(test_repo).setup_git() # Create a fake .gitmodules file (this simulates what happens after cloning without --recurse-submodules) gitmodules_content = """[submodule "external"] diff --git a/testing/test_integration.py b/testing/test_integration.py index be6e3cfe..ad60d042 100644 --- a/testing/test_integration.py +++ b/testing/test_integration.py @@ -34,14 +34,11 @@ c = Configuration() +# Module-level fixture for git SCM setup @pytest.fixture -def wd(wd: WorkDir) -> WorkDir: - wd("git init") - wd("git config user.email test@example.com") - wd('git config user.name "a test"') - wd.add_command = "git add ." - wd.commit_command = "git commit -m test-{reason}" - return wd +def wd(wd: WorkDir, monkeypatch: pytest.MonkeyPatch) -> WorkDir: + """Set up git for integration tests.""" + return wd.setup_git(monkeypatch) def test_pyproject_support(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: diff --git a/testing/test_main.py b/testing/test_main.py index 3c0ff3f8..cd21b6d8 100644 --- a/testing/test_main.py +++ b/testing/test_main.py @@ -22,11 +22,7 @@ def test_main() -> None: @pytest.fixture def repo(wd: WorkDir) -> WorkDir: - wd("git init") - wd("git config user.email user@host") - wd("git config user.name user") - wd.add_command = "git add ." - wd.commit_command = "git commit -m test-{reason}" + wd.setup_git() wd.write("README.rst", "My example") wd.add_and_commit() diff --git a/testing/test_mercurial.py b/testing/test_mercurial.py index 3e15aae8..effc5657 100644 --- a/testing/test_mercurial.py +++ b/testing/test_mercurial.py @@ -10,23 +10,18 @@ from setuptools_scm import Configuration from setuptools_scm._run_cmd import CommandNotFoundError -from setuptools_scm._run_cmd import has_command from setuptools_scm.hg import archival_to_version from setuptools_scm.hg import parse from setuptools_scm.version import format_version from testing.wd_wrapper import WorkDir -pytestmark = pytest.mark.skipif( - not has_command("hg", warn=False), reason="hg executable not found" -) +# Note: Mercurial availability is now checked in WorkDir.setup_hg() method @pytest.fixture def wd(wd: WorkDir) -> WorkDir: - wd("hg init") - wd.add_command = "hg add ." - wd.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"' - return wd + """Set up mercurial for hg-specific tests.""" + return wd.setup_hg() archival_mapping = { diff --git a/testing/wd_wrapper.py b/testing/wd_wrapper.py index 1f5efbe7..98c8f882 100644 --- a/testing/wd_wrapper.py +++ b/testing/wd_wrapper.py @@ -3,8 +3,16 @@ import itertools from pathlib import Path +from typing import TYPE_CHECKING from typing import Any +import pytest + +from setuptools_scm._run_cmd import has_command + +if TYPE_CHECKING: + pass + class WorkDir: """a simple model for a""" @@ -12,6 +20,7 @@ class WorkDir: commit_command: str signed_commit_command: str add_command: str + tag_command: str def __repr__(self) -> str: return f"" @@ -68,3 +77,123 @@ def get_version(self, **kw: Any) -> str: version = get_version(root=self.cwd, fallback_root=self.cwd, **kw) print(self.cwd.name, version, sep=": ") return version + + def create_basic_setup_py( + self, name: str = "test-package", use_scm_version: str = "True" + ) -> None: + """Create a basic setup.py file with setuptools_scm configuration.""" + self.write( + "setup.py", + f"""__import__('setuptools').setup( + name="{name}", + use_scm_version={use_scm_version}, +)""", + ) + + def create_basic_pyproject_toml( + self, name: str = "test-package", dynamic_version: bool = True + ) -> None: + """Create a basic pyproject.toml file with setuptools_scm configuration.""" + dynamic_section = 'dynamic = ["version"]' if dynamic_version else "" + self.write( + "pyproject.toml", + f"""[build-system] +requires = ["setuptools>=64", "setuptools_scm>=8"] +build-backend = "setuptools.build_meta" + +[project] +name = "{name}" +{dynamic_section} + +[tool.setuptools_scm] +""", + ) + + def create_basic_setup_cfg(self, name: str = "test-package") -> None: + """Create a basic setup.cfg file with metadata.""" + self.write( + "setup.cfg", + f"""[metadata] +name = {name} +""", + ) + + def create_test_file( + self, filename: str = "test.txt", content: str = "test content" + ) -> None: + """Create a test file and commit it to the repository.""" + # Create parent directories if they don't exist + path = self.cwd / filename + path.parent.mkdir(parents=True, exist_ok=True) + self.write(filename, content) + self.add_and_commit() + + def create_tag(self, tag: str = "1.0.0") -> None: + """Create a tag using the configured tag_command.""" + if hasattr(self, "tag_command"): + self(self.tag_command, tag=tag) + else: + raise RuntimeError("No tag_command configured") + + def configure_git_commands(self) -> None: + """Configure git commands without initializing the repository.""" + self.add_command = "git add ." + self.commit_command = "git commit -m test-{reason}" + self.tag_command = "git tag {tag}" + + def configure_hg_commands(self) -> None: + """Configure mercurial commands without initializing the repository.""" + self.add_command = "hg add ." + self.commit_command = 'hg commit -m test-{reason} -u test -d "0 0"' + self.tag_command = "hg tag {tag}" + + def setup_git( + self, monkeypatch: pytest.MonkeyPatch | None = None, *, init: bool = True + ) -> WorkDir: + """Set up git SCM for this WorkDir. + + Args: + monkeypatch: Optional pytest MonkeyPatch to clear HOME environment + init: Whether to initialize the git repository (default: True) + + Returns: + Self for method chaining + + Raises: + pytest.skip: If git executable is not found + """ + if not has_command("git", warn=False): + pytest.skip("git executable not found") + + self.configure_git_commands() + + if init: + if monkeypatch: + monkeypatch.delenv("HOME", raising=False) + self("git init") + self("git config user.email test@example.com") + self('git config user.name "a test"') + + return self + + def setup_hg(self, *, init: bool = True) -> WorkDir: + """Set up mercurial SCM for this WorkDir. + + Args: + init: Whether to initialize the mercurial repository (default: True) + + Returns: + Self for method chaining + + Raises: + pytest.skip: If hg executable is not found + """ + if not has_command("hg", warn=False): + pytest.skip("hg executable not found") + + self.configure_hg_commands() + + if init: + self("hg init") + + return self