Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ jobs:
run: uv run ruff check

- name: Run tests with pytest
run: pytest -v tests/
run: |
pip cache purge
pytest -v tests/

build:
name: Build Package
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ build-backend = "hatchling.build"
[tool.hatch.build.hooks.hatch-build-freeze]
```

## Disable the plugin
By default, the plugin is enabled. To disable it, use the environment variable `HATCH_BUILD_FREEZE_DISABLED`

```Shell
HATCH_BUILD_FREEZE_DISABLED=1 hatch build
```

## Plugin Configuration


Expand All @@ -50,5 +57,5 @@ The following options are supported:
A list of additional command-line arguments to pass directly to the `uv export` command.
```toml
[tool.hatch.build.hooks.hatch-build-freeze]
uv-args = ["--resolution=lowest-direct", "--no-header"]
uv-args = ["--resolution=lowest-direct", "--no-header", "--prerelease=allow"]
```
7 changes: 6 additions & 1 deletion hatch_build_freeze/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

from __future__ import annotations

import os
import subprocess
from pathlib import Path
from typing import TYPE_CHECKING, Any
Expand Down Expand Up @@ -85,7 +86,6 @@ def _generate_requirements_file(self) -> bool:
"export",
"--locked",
"--format=requirements.txt",
"--prerelease=allow",
"--output-file",
str(self.requirements_file_path),
"--no-editable",
Expand Down Expand Up @@ -139,6 +139,11 @@ def initialize(self, version: str, build_data: dict[str, Any]) -> None:

Generates requirements.txt, then includes its dependencies.
"""
if os.getenv("HATCH_BUILD_FREEZE_DISABLED", "0").lower() not in ("0", "false"):
self.logger.info(
"Hatch Build Freeze is disabled. Set HATCH_BUILD_FREEZE_DISABLED=0 to enable."
)
return
generation_successful = self._generate_requirements_file()

if not generation_successful and not self.requirements_file_path.exists():
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "hatch-build-freeze"
version = "1.0.1"
version = "1.1.0"
description = "Hatch Dependency Freezing Plugin: Enhancing Build Consistency"
authors = [{ name = "minds.ai, Inc.", email = "contact@minds.ai" }]
readme = "README.md"
Expand Down
77 changes: 66 additions & 11 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# ==============================================================================
"""Tests for Hatch build freeze plugin."""

import os
import subprocess
import tarfile
import zipfile
Expand Down Expand Up @@ -92,21 +93,48 @@ def base_project_structure(tmp_path: Path, request) -> Path:
return project_dir


def returns_pyproject_data_and_lock(mock_uv_project: Path) -> tuple[dict, str | None]:
"""Returns the TOML data and the prerelease argument if it exists."""
toml_data = tomlkit.loads((mock_uv_project / "pyproject.toml").read_text())
freeze_config = toml_data["tool"]["hatch"]["build"]["hooks"]["hatch-build-freeze"]
prerelease_arg = next(
(arg for arg in freeze_config.get("uv-args", []) if arg.startswith("--prerelease")),
None,
)
if prerelease_arg:
subprocess.check_output(["uv", "lock", prerelease_arg], cwd=mock_uv_project)
else:
subprocess.check_output(["uv", "lock"], cwd=mock_uv_project)
return toml_data, prerelease_arg


@pytest.mark.parametrize(
"mock_uv_project",
[
pytest.param({"groups": ["g1"], "extras": ["e1"]}, id="with_groups_and_extras"),
pytest.param({"groups": ["g1"]}, id="with_groups"),
pytest.param({"extras": ["e1"]}, id="with_extras"),
pytest.param({"groups": []}, id="empty_groups"),
pytest.param(
{"groups": ["g1"], "extras": ["e1"], "uv-args": ["--prerelease=allow"]},
id="with_groups_and_extras_prerelease",
),
pytest.param(
{"groups": ["g1"], "uv-args": ["--prerelease=allow"]}, id="with_groups_prerelease"
),
pytest.param(
{"extras": ["e1"], "uv-args": ["--prerelease=allow"]}, id="with_extras_prerelease"
),
pytest.param(
{"groups": [], "uv-args": ["--prerelease=allow"]}, id="empty_groups_prerelease"
),
],
indirect=True,
)
def test_build_hook(mock_uv_project: Path, request) -> None:
"""Tests the standalone implementation of the build hook."""
syspath.insert(0, str(mock_uv_project))
subprocess.check_output(["uv", "lock", "--prerelease=allow"], cwd=mock_uv_project)
toml_data = tomlkit.loads((mock_uv_project / "pyproject.toml").read_text())
toml_data, prerelease_arg = returns_pyproject_data_and_lock(mock_uv_project)
hook = plugin.HatchBuildFreezePlugin(
mock_uv_project,
toml_data["tool"]["hatch"]["build"]["hooks"]["hatch-build-freeze"],
Expand All @@ -125,15 +153,15 @@ def test_build_hook(mock_uv_project: Path, request) -> None:
"colorama==0.4.6 ; sys_platform == 'win32'",
"tqdm==4.67.1",
]
if request.node.callspec.id == "with_groups_and_extras":
if request.node.callspec.id in ("with_groups_and_extras", "with_groups_and_extras_prerelease"):
expected_dependencies.extend(["click==8.2.1", "psutil==6.1.1"])
elif request.node.callspec.id == "with_groups":
elif request.node.callspec.id in ("with_groups", "with_groups_prerelease"):
expected_dependencies.append("click==8.2.1")
elif request.node.callspec.id == "with_extras":
elif request.node.callspec.id in ("with_extras", "with_extras_prerelease"):
expected_dependencies.append("psutil==6.1.1")

assert set(dependencies) == set(expected_dependencies)
assert not hook.uv_args
assert not hook.uv_args if not prerelease_arg else hook.uv_args == ["--prerelease=allow"]
assert hook.requirements_file_path.exists()
version = "0.1.0"
hook.initialize(version, {})
Expand All @@ -151,12 +179,17 @@ def verify_dependencies(pkg_data_bytes: bytes, test_type: str) -> None:
"tqdm==4.67.1",
"psutil==6.1.1; extra == 'e1'",
}
if test_type == "with_groups_and_extras":
if test_type in ("with_groups_and_extras", "with_groups_and_extras_prerelease"):
expected_dependencies.update({"click==8.2.1", "psutil==6.1.1"})
elif test_type == "with_groups":
elif test_type in ("with_groups", "with_groups_prerelease"):
expected_dependencies.add("click==8.2.1")
elif test_type == "with_extras":
elif test_type in ("with_extras", "with_extras_prerelease"):
expected_dependencies.add("psutil==6.1.1")
elif "dont-freeze" in test_type:
expected_dependencies = {
"tqdm<=4.67.1",
"psutil==6.1.1; extra == 'e1'",
}
assert dependencies == expected_dependencies


Expand All @@ -167,14 +200,36 @@ def verify_dependencies(pkg_data_bytes: bytes, test_type: str) -> None:
pytest.param({"groups": ["g1"]}, id="with_groups"),
pytest.param({"extras": ["e1"]}, id="with_extras"),
pytest.param({"groups": []}, id="empty_groups"),
pytest.param(
{"groups": ["g1"], "extras": ["e1"], "uv-args": ["--prerelease=allow"]},
id="with_groups_and_extras_prerelease",
),
pytest.param(
{"groups": ["g1"], "uv-args": ["--prerelease=allow"]}, id="with_groups_prerelease"
),
pytest.param(
{"extras": ["e1"], "uv-args": ["--prerelease=allow"]}, id="with_extras_prerelease"
),
pytest.param(
{"groups": [], "uv-args": ["--prerelease=allow"]}, id="empty_groups_prerelease"
),
pytest.param({"groups": ["g1"], "extras": ["e1"]}, id="dont-freeze-with-groups-and-extras"),
pytest.param({"groups": ["g1"]}, id="dont-freeze-with-groups"),
pytest.param({"extras": ["e1"]}, id="dont-freeze-with-extras"),
pytest.param({"groups": []}, id="dont-freeze"),
],
indirect=True,
)
def test_build(mock_uv_project: Path, request) -> None:
"""Tests the build process on a sample project with the plugin."""
syspath.insert(0, str(mock_uv_project))
subprocess.check_output(["uv", "lock", "--prerelease=allow"], cwd=mock_uv_project)
subprocess.check_output(["hatch", "build"], cwd=mock_uv_project)
env = os.environ.copy()
if "dont-freeze" in request.node.callspec.id:
env["HATCH_BUILD_FREEZE_DISABLED"] = "1"
else:
env["HATCH_BUILD_FREEZE_DISABLED"] = "0"
returns_pyproject_data_and_lock(mock_uv_project)
subprocess.check_output(["hatch", "-v", "build"], cwd=mock_uv_project, env=env)
wheel_path = mock_uv_project / "dist" / "my_test_package-0.1.0-py3-none-any.whl"
sdist_path = mock_uv_project / "dist" / "my_test_package-0.1.0.tar.gz"
assert wheel_path.exists()
Expand Down
Loading