diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 0d4a882..4bbfe54 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -48,3 +48,13 @@ pass_filenames: false additional_dependencies: [] minimum_pre_commit_version: "2.9.2" +- id: uv-update-requirements + name: uv-update-requirements + description: "Run 'uv export' and stage the output file so the commit is not interrupted" + entry: uv-update-requirements + language: python + files: ^(uv\.lock|pyproject\.toml|uv\.toml)$ + args: ["--frozen", "--output-file=requirements.txt", "--quiet"] + pass_filenames: false + additional_dependencies: [] + minimum_pre_commit_version: "2.9.2" diff --git a/README.md b/README.md index 6e1056d..e1ba858 100644 --- a/README.md +++ b/README.md @@ -147,8 +147,34 @@ include it as an additional sync dependency: additional_dependencies: [keyring] ``` +To export `uv.lock` to `requirements.txt` **without interrupting the commit**: + +```yaml +- repo: https://github.com/astral-sh/uv-pre-commit + # uv version. + rev: 0.11.29 + hooks: + - id: uv-update-requirements +``` + +Unlike `uv-export`, this hook automatically stages the generated `requirements.txt` +so that pre-commit does not abort the commit with a *"files were modified by this hook"* +error requiring a second `git commit`. + +To export to a custom file, override the `args`: + +```yaml +- repo: https://github.com/astral-sh/uv-pre-commit + # uv version. + rev: 0.11.29 + hooks: + - id: uv-update-requirements + args: ["--frozen", "--output-file=requirements-prod.txt", "--quiet"] +``` + ## License + uv-pre-commit is licensed under either of - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) diff --git a/pyproject.toml b/pyproject.toml index 4f94a7b..ebe0bb2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,3 +10,6 @@ dev = [ "packaging~=23.1", "urllib3>=2.0.5,<3.0.0", ] + +[project.scripts] +uv-update-requirements = "uv_update_requirements:main" diff --git a/uv_update_requirements.py b/uv_update_requirements.py new file mode 100644 index 0000000..c81d03c --- /dev/null +++ b/uv_update_requirements.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +"""Hook: regenerate requirements.txt (or a custom output file) via `uv export` +and automatically stage it so the commit is not interrupted by pre-commit's +"files were modified by this hook" check. + +Usage (via .pre-commit-config.yaml): + - id: uv-update-requirements + args: ["--frozen", "--output-file=requirements.txt", "--quiet"] +""" +from __future__ import annotations + +import subprocess +import sys + +def _parse_output_file(args: list[str]) -> str: + """Extract the --output-file value from the args list.""" + for i, arg in enumerate(args): + if arg.startswith("--output-file="): + return arg.split("=", 1)[1] + if arg == "--output-file" and i + 1 < len(args): + return args[i + 1] + return "requirements.txt" + +def main(argv: list[str] | None = None) -> int: + args = argv if argv is not None else sys.argv[1:] + + # Fall back to sensible defaults when no args are supplied. + if not args: + args = ["--frozen", "--output-file=requirements.txt", "--quiet"] + + # Run `uv export` with the provided arguments. + export_result = subprocess.run(["uv", "export", *args]) + if export_result.returncode != 0: + return export_result.returncode + + # Stage the generated file so pre-commit does not abort the commit. + output_file = _parse_output_file(args) + stage_result = subprocess.run(["git", "add", output_file]) + if stage_result.returncode != 0: + print( + f"uv-update-requirements: warning: could not stage '{output_file}' " + f"(git add exited with {stage_result.returncode})", + file=sys.stderr, + ) + return stage_result.returncode + + return 0 + +if __name__ == "__main__": + sys.exit(main())