diff --git a/src/versioningit/onbuild.py b/src/versioningit/onbuild.py index b992fd1..496193b 100644 --- a/src/versioningit/onbuild.py +++ b/src/versioningit/onbuild.py @@ -112,14 +112,17 @@ def open( p = self.provider.build_dir / path if ("w" in mode or "a" in mode) and path not in self.provider.modified: self.provider.modified.add(path) + p.parent.mkdir(parents=True, exist_ok=True) # If setuptools is using hard links for the build files, undo that # for this file: - with suppress(FileNotFoundError): + try: p.unlink() - p.parent.mkdir(parents=True, exist_ok=True) - if not p.exists() and "a" in mode: - with suppress(FileNotFoundError): - shutil.copy2(self.provider.src_dir / self.source_path, p) + except FileNotFoundError: + pass + else: + if "a" in mode: + with suppress(FileNotFoundError): + shutil.copy2(self.provider.src_dir / self.source_path, p) return p.open(mode=mode, encoding=encoding, errors=errors, newline=newline) diff --git a/test/test_methods/test_onbuild.py b/test/test_methods/test_onbuild.py index da79716..1211ba1 100644 --- a/test/test_methods/test_onbuild.py +++ b/test/test_methods/test_onbuild.py @@ -1,11 +1,20 @@ from __future__ import annotations from datetime import datetime, timezone +import os from pathlib import Path from shutil import copytree -from typing import Any +from typing import TYPE_CHECKING, Any import pytest from versioningit.errors import ConfigError -from versioningit.onbuild import SetuptoolsFileProvider, replace_version_onbuild +from versioningit.onbuild import ( + FileProvider, + HatchFileProvider, + SetuptoolsFileProvider, + replace_version_onbuild, +) + +if TYPE_CHECKING: + from typing_extensions import Literal DATA_DIR = Path(__file__).parent.with_name("data") @@ -251,3 +260,158 @@ def test_replace_version_onbuild_version_not_captured(tmp_path: Path) -> None: ) for p in tmp_path.iterdir(): assert p.read_bytes() == (src_dir / p.name).read_bytes() + + +@pytest.mark.parametrize("is_source", [False, True]) +@pytest.mark.parametrize( + "mode,after", + [ + ("w", "Coconut\n"), + ("a", "Apple\nCoconut\n"), + ], +) +def test_setuptools_file_provider_read_write_read( + is_source: bool, mode: Literal["w", "a"], after: str, tmp_path: Path +) -> None: + src_dir = tmp_path / "src" + src_dir.mkdir() + build_dir = tmp_path / "build" + build_dir.mkdir() + (src_dir / "apple.txt").write_text("Apple\n") + (build_dir / "apple.txt").write_text("Macintosh\n") + (build_dir / "banana.txt").write_text("Banana\n") + provider = SetuptoolsFileProvider(src_dir=src_dir, build_dir=build_dir) + file = provider.get_file( + source_path="apple.txt", build_path="banana.txt", is_source=is_source + ) + with file.open() as fp: + contents = fp.read() + if is_source: + assert contents == "Macintosh\n" + else: + assert contents == "Banana\n" + with file.open(mode) as fp: + fp.write("Coconut\n") + with file.open() as fp: + contents = fp.read() + assert contents == after + assert (src_dir / "apple.txt").read_text() == "Apple\n" + if is_source: + assert (build_dir / "apple.txt").read_text() == after + assert (build_dir / "banana.txt").read_text() == "Banana\n" + else: + assert (build_dir / "apple.txt").read_text() == "Macintosh\n" + assert (build_dir / "banana.txt").read_text() == after + + +@pytest.mark.parametrize("is_source", [False, True]) +@pytest.mark.parametrize( + "mode,after", + [ + ("w", "Coconut\n"), + ("a", "Apple\nCoconut\n"), + ], +) +def test_setuptools_file_provider_read_write_read_hard_link( + is_source: bool, mode: Literal["w", "a"], after: str, tmp_path: Path +) -> None: + src_dir = tmp_path / "src" + src_dir.mkdir() + build_dir = tmp_path / "build" + build_dir.mkdir() + (src_dir / "apple.txt").write_text("Apple\n") + if is_source: + os.link(src_dir / "apple.txt", build_dir / "apple.txt") + else: + os.link(src_dir / "apple.txt", build_dir / "banana.txt") + provider = SetuptoolsFileProvider(src_dir=src_dir, build_dir=build_dir) + file = provider.get_file( + source_path="apple.txt", build_path="banana.txt", is_source=is_source + ) + with file.open() as fp: + contents = fp.read() + assert contents == "Apple\n" + with file.open(mode) as fp: + fp.write("Coconut\n") + with file.open() as fp: + contents = fp.read() + assert contents == after + assert (src_dir / "apple.txt").read_text() == "Apple\n" + if is_source: + assert (build_dir / "apple.txt").read_text() == after + assert not (build_dir / "banana.txt").exists() + else: + assert not (build_dir / "apple.txt").exists() + assert (build_dir / "banana.txt").read_text() == after + + +@pytest.mark.parametrize("is_source", [False, True]) +@pytest.mark.parametrize( + "mode,after", + [ + ("w", "Banana\n"), + ("a", "Apple\nBanana\n"), + ], +) +def test_hatch_file_provider_read_write_read( + is_source: bool, mode: Literal["w", "a"], after: str, tmp_path: Path +) -> None: + src_dir = tmp_path / "src" + src_dir.mkdir() + tmp_dir = tmp_path / "tmp" + tmp_dir.mkdir() + (src_dir / "apple.txt").write_text("Apple\n") + provider = HatchFileProvider(src_dir=src_dir, tmp_dir=tmp_dir) + file = provider.get_file( + source_path="apple.txt", build_path="banana.txt", is_source=is_source + ) + with file.open() as fp: + contents = fp.read() + assert contents == "Apple\n" + with file.open(mode) as fp: + fp.write("Banana\n") + with file.open() as fp: + contents = fp.read() + assert contents == after + assert (src_dir / "apple.txt").read_text() == "Apple\n" + if is_source: + assert (tmp_dir / "apple.txt").read_text() == after + assert not (tmp_dir / "banana.txt").exists() + else: + assert not (tmp_dir / "apple.txt").exists() + assert (tmp_dir / "banana.txt").read_text() == after + + +@pytest.mark.parametrize("backend", ["setuptools", "hatch"]) +@pytest.mark.parametrize("is_source", [False, True]) +@pytest.mark.parametrize("mode", ["w", "a"]) +def test_file_provider_write_new_file( + backend: str, is_source: bool, mode: Literal["w", "a"], tmp_path: Path +) -> None: + src_dir = tmp_path / "src" + src_dir.mkdir() + build_dir = tmp_path / "build" + build_dir.mkdir() + provider: FileProvider + if backend == "setuptools": + provider = SetuptoolsFileProvider(src_dir=src_dir, build_dir=build_dir) + else: + assert backend == "hatch" + provider = HatchFileProvider(src_dir=src_dir, tmp_dir=build_dir) + file = provider.get_file( + source_path="green/apple.txt", + build_path="yellow/banana.txt", + is_source=is_source, + ) + with file.open(mode) as fp: + fp.write("Coconut\n") + with file.open() as fp: + contents = fp.read() + assert contents == "Coconut\n" + assert not (src_dir / "green" / "apple.txt").exists() + if is_source: + assert (build_dir / "green" / "apple.txt").read_text() == "Coconut\n" + assert not (build_dir / "yellow" / "banana.txt").exists() + else: + assert not (build_dir / "green" / "apple.txt").exists() + assert (build_dir / "yellow" / "banana.txt").read_text() == "Coconut\n"