├── src ├── pre_commit_uv │ ├── py.typed │ └── __init__.py └── pre_commit_uv_patch.pth ├── .github ├── FUNDING.yaml ├── release.yml ├── dependabot.yaml ├── SECURITY.md └── workflows │ ├── release.yaml │ └── check.yaml ├── .gitignore ├── task └── dev_pth.py ├── LICENSE.txt ├── .pre-commit-config.yaml ├── tox.ini ├── README.md ├── CODE_OF_CONDUCT.md ├── pyproject.toml └── tests └── test_main.py /src/pre_commit_uv/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yaml: -------------------------------------------------------------------------------- 1 | tidelift: "pypi/pre-commit-uv" 2 | -------------------------------------------------------------------------------- /src/pre_commit_uv_patch.pth: -------------------------------------------------------------------------------- 1 | import pre_commit_uv; pre_commit_uv._patch() 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | .tox/ 3 | .*_cache 4 | __pycache__ 5 | **.pyc 6 | dist 7 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - dependabot[bot] 5 | - pre-commit-ci[bot] 6 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /task/dev_pth.py: -------------------------------------------------------------------------------- 1 | """For editable installs the pth file is not applied, so we have to manually add it.""" # noqa: INP001 2 | 3 | from __future__ import annotations 4 | 5 | import shutil 6 | import sys 7 | from pathlib import Path 8 | 9 | ROOT = Path(__file__).parents[1] 10 | shutil.copy2(ROOT / "src" / "pre_commit_uv_patch.pth", sys.argv[1]) 11 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 4+ | :white_check_mark: | 8 | | <4 | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift 13 | will coordinate the fix and disclosure. 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a 2 | copy of this software and associated documentation files (the 3 | "Software"), to deal in the Software without restriction, including 4 | without limitation the rights to use, copy, modify, merge, publish, 5 | distribute, sublicense, and/or sell copies of the Software, and to 6 | permit persons to whom the Software is furnished to do so, subject to 7 | the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included 10 | in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 13 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 14 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 15 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 16 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 17 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 18 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v6.0.0 4 | hooks: 5 | - id: end-of-file-fixer 6 | - id: trailing-whitespace 7 | - repo: https://github.com/python-jsonschema/check-jsonschema 8 | rev: 0.35.0 9 | hooks: 10 | - id: check-github-workflows 11 | args: ["--verbose"] 12 | - repo: https://github.com/codespell-project/codespell 13 | rev: v2.4.1 14 | hooks: 15 | - id: codespell 16 | args: ["--write-changes"] 17 | - repo: https://github.com/tox-dev/tox-ini-fmt 18 | rev: "1.7.0" 19 | hooks: 20 | - id: tox-ini-fmt 21 | args: ["-p", "fix"] 22 | - repo: https://github.com/tox-dev/pyproject-fmt 23 | rev: "v2.11.1" 24 | hooks: 25 | - id: pyproject-fmt 26 | - repo: https://github.com/astral-sh/ruff-pre-commit 27 | rev: "v0.14.8" 28 | hooks: 29 | - id: ruff-format 30 | - id: ruff 31 | args: ["--fix", "--unsafe-fixes", "--exit-non-zero-on-fix"] 32 | - repo: meta 33 | hooks: 34 | - id: check-hooks-apply 35 | - id: check-useless-excludes 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release to PyPI 2 | on: 3 | push: 4 | tags: ["*"] 5 | 6 | env: 7 | dists-artifact-name: python-package-distributions 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v6 14 | with: 15 | fetch-depth: 0 16 | - name: Install the latest version of uv 17 | uses: astral-sh/setup-uv@v7 18 | with: 19 | enable-cache: true 20 | cache-dependency-glob: "pyproject.toml" 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | - name: Build package 23 | run: uv build --python 3.14 --python-preference only-managed --sdist --wheel . --out-dir dist 24 | - name: Store the distribution packages 25 | uses: actions/upload-artifact@v5 26 | with: 27 | name: ${{ env.dists-artifact-name }} 28 | path: dist/* 29 | 30 | release: 31 | needs: 32 | - build 33 | runs-on: ubuntu-latest 34 | environment: 35 | name: release 36 | url: https://pypi.org/project/pre-commit-uv/${{ github.ref_name }} 37 | permissions: 38 | id-token: write 39 | steps: 40 | - name: Download all the dists 41 | uses: actions/download-artifact@v6 42 | with: 43 | name: ${{ env.dists-artifact-name }} 44 | path: dist/ 45 | - name: Publish to PyPI 46 | uses: pypa/gh-action-pypi-publish@v1.13.0 47 | with: 48 | attestations: true 49 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | name: check 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: ["main"] 6 | tags-ignore: ["**"] 7 | pull_request: 8 | schedule: 9 | - cron: "0 8 * * *" 10 | 11 | concurrency: 12 | group: check-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | env: 22 | - "3.14" 23 | - "3.13" 24 | - "3.12" 25 | - "3.11" 26 | - "3.10" 27 | - type 28 | - dev 29 | - pkg_meta 30 | steps: 31 | - uses: actions/checkout@v6 32 | with: 33 | fetch-depth: 0 34 | - name: Install the latest version of uv 35 | uses: astral-sh/setup-uv@v7 36 | with: 37 | enable-cache: true 38 | cache-dependency-glob: "pyproject.toml" 39 | - name: Install tox 40 | run: uv tool install --python-preference only-managed --python 3.14 tox --with tox-uv 41 | - name: Install Python 42 | if: startsWith(matrix.env, '3.') && matrix.env != '3.14' 43 | run: uv python install --python-preference only-managed ${{ matrix.env }} 44 | - name: Setup test suite 45 | run: tox run -vv --notest --skip-missing-interpreters false -e ${{ matrix.env }} 46 | - name: Run test suite 47 | run: tox run --skip-pkg-install -e ${{ matrix.env }} 48 | env: 49 | PYTEST_ADDOPTS: "-vv --durations=5" 50 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | requires = 3 | tox>=4.31 4 | tox-uv>=1.28.1 5 | env_list = 6 | fix 7 | 3.14 8 | 3.13 9 | 3.12 10 | 3.11 11 | 3.10 12 | 3.9 13 | 3.8 14 | type 15 | pkg_meta 16 | skip_missing_interpreters = true 17 | 18 | [testenv] 19 | description = run the unit tests with pytest under {base_python} 20 | package = wheel 21 | wheel_build_env = .pkg 22 | extras = 23 | testing 24 | pass_env = 25 | PYTEST_* 26 | set_env = 27 | COVERAGE_FILE = {work_dir}/.coverage.{env_name} 28 | commands = 29 | python -m pytest {tty:--color=yes} {posargs: \ 30 | --cov {env_site_packages_dir}{/}pre_commit_uv --cov {tox_root}{/}tests \ 31 | --cov-config=pyproject.toml --no-cov-on-fail --cov-report term-missing:skip-covered --cov-context=test \ 32 | --cov-report html:{env_tmp_dir}{/}htmlcov --cov-report xml:{work_dir}{/}coverage.{env_name}.xml \ 33 | --junitxml {work_dir}{/}junit.{env_name}.xml \ 34 | tests} 35 | 36 | [testenv:fix] 37 | description = format the code base to adhere to our styles, and complain about what we cannot do automatically 38 | skip_install = true 39 | deps = 40 | pre-commit-uv>=4.1.1 41 | commands = 42 | pre-commit run --all-files --show-diff-on-failure 43 | 44 | [testenv:type] 45 | description = run type check on code base 46 | deps = 47 | mypy==1.18.2 48 | commands = 49 | mypy src 50 | mypy tests 51 | 52 | [testenv:pkg_meta] 53 | description = check that the long description is valid 54 | skip_install = true 55 | deps = 56 | check-wheel-contents>=0.6.3 57 | twine>=6.2 58 | uv>=0.9.1 59 | commands = 60 | uv build --sdist --wheel --out-dir {env_tmp_dir} . 61 | twine check {env_tmp_dir}{/}* 62 | check-wheel-contents --no-config {env_tmp_dir} 63 | 64 | [testenv:dev] 65 | description = generate a DEV environment 66 | package = editable 67 | commands = 68 | uv tree 69 | python -c 'import sys; print(sys.executable)' 70 | python task/dev_pth.py {env_site_packages_dir} 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pre-commit-uv 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/pre-commit-uv?style=flat-square)](https://pypi.org/project/pre-commit-uv) 4 | [![PyPI - Implementation](https://img.shields.io/pypi/implementation/pre-commit-uv?style=flat-square)](https://pypi.org/project/pre-commit-uv) 5 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pre-commit-uv?style=flat-square)](https://pypi.org/project/pre-commit-uv) 6 | [![Downloads](https://static.pepy.tech/badge/pre-commit-uv/month)](https://pepy.tech/project/pre-commit-uv) 7 | [![PyPI - License](https://img.shields.io/pypi/l/pre-commit-uv?style=flat-square)](https://opensource.org/licenses/MIT) 8 | [![check](https://github.com/tox-dev/pre-commit-uv/actions/workflows/check.yaml/badge.svg)](https://github.com/tox-dev/pre-commit-uv/actions/workflows/check.yaml) 9 | 10 | Use `uv` to create virtual environments and install packages for `pre-commit`. 11 | 12 | ## Installation 13 | 14 | With pipx: 15 | 16 | ```shell 17 | pipx install pre-commit 18 | pipx inject pre-commit pre-commit-uv 19 | ``` 20 | 21 | With uv: 22 | 23 | ```shell 24 | uv tool install pre-commit --with pre-commit-uv --force-reinstall 25 | ``` 26 | 27 | ## Why? 28 | 29 | Compared to upstream `pre-commit` will speed up the initial seed operation. In general, upstream recommends caching the 30 | `pre-commit` cache, however, that is not always possible and is still helpful to have a more performant initial cache 31 | creation., Here's an example of what you could expect demonstrated on this project's own pre-commit setup (with a hot 32 | `uv` cache): 33 | 34 | ```shell 35 | ❯ hyperfine 'pre-commit install-hooks' 'pre-commit-uv install-hooks' 36 | Benchmark 1: pre-commit install-hooks 37 | Time (mean ± σ): 54.132 s ± 8.827 s [User: 15.424 s, System: 9.359 s] 38 | Range (min … max): 45.972 s … 66.506 s 10 runs 39 | 40 | Benchmark 2: pre-commit-uv install-hooks 41 | Time (mean ± σ): 41.695 s ± 7.395 s [User: 7.614 s, System: 6.133 s] 42 | Range (min … max): 32.198 s … 58.467 s 10 runs 43 | 44 | Summary 45 | pre-commit-uv install-hooks ran 1.30 ± 0.31 times faster than pre-commit install-hooks 46 | ``` 47 | 48 | ## Configuration 49 | 50 | Once installed will use `uv` out of box, however the `DISABLE_PRE_COMMIT_UV_PATCH` environment variable if is set it 51 | will work as an escape hatch to disable the new behavior. 52 | 53 | To avoid interpreter startup overhead of the patching, we only perform this when we detect you calling `pre-commit`. 54 | Should this logic fail you can force the patching by setting the `FORCE_PRE_COMMIT_UV_PATCH` variable. Should you 55 | experience this please raise an issue with the content of the `sys.argv`. Note that `DISABLE_PRE_COMMIT_UV_PATCH` will 56 | overwrite this flag should both be set. 57 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making 6 | participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, 7 | disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, 8 | religion, or sexual identity and orientation. 9 | 10 | ## Our Standards 11 | 12 | Examples of behavior that contributes to creating a positive environment include: 13 | 14 | - Using welcoming and inclusive language 15 | - Being respectful of differing viewpoints and experiences 16 | - Gracefully accepting constructive criticism 17 | - Focusing on what is best for the community 18 | - Showing empathy towards other community members 19 | 20 | Examples of unacceptable behavior by participants include: 21 | 22 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 23 | - Trolling, insulting/derogatory comments, and personal or political attacks 24 | - Public or private harassment 25 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 26 | - Other conduct which could reasonably be considered inappropriate in a professional setting 27 | 28 | ## Our Responsibilities 29 | 30 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take 31 | appropriate and fair corrective action in response to any instances of unacceptable behavior. 32 | 33 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, 34 | issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any 35 | contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 36 | 37 | ## Scope 38 | 39 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the 40 | project or its community. Examples of representing a project or community include using an official project e-mail 41 | address, posting via an official social media account, or acting as an appointed representative at an online or offline 42 | event. Representation of a project may be further defined and clarified by project maintainers. 43 | 44 | ## Enforcement 45 | 46 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at 47 | gaborbernat@python.org. The project team will review and investigate all complaints, and will respond in a way that it 48 | deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the 49 | reporter of an incident. Further details of specific enforcement policies may be posted separately. 50 | 51 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent 52 | repercussions as determined by other members of the project's leadership. 53 | 54 | ## Attribution 55 | 56 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at 57 | [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html][version] 58 | 59 | [homepage]: https://www.contributor-covenant.org/ 60 | [version]: https://www.contributor-covenant.org/version/1/4/ 61 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = [ 4 | "hatch-vcs>=0.5", 5 | "hatchling>=1.27", 6 | ] 7 | 8 | [project] 9 | name = "pre-commit-uv" 10 | description = "Run pre-commit with uv" 11 | readme = "README.md" 12 | keywords = [ 13 | "format", 14 | "pyproject", 15 | ] 16 | license.file = "LICENSE.txt" 17 | authors = [ 18 | { name = "Bernat Gabor", email = "gaborjbernat@gmail.com" }, 19 | ] 20 | requires-python = ">=3.10" 21 | classifiers = [ 22 | "License :: OSI Approved :: MIT License", 23 | "Operating System :: OS Independent", 24 | "Programming Language :: Python", 25 | "Programming Language :: Python :: 3 :: Only", 26 | "Programming Language :: Python :: 3.10", 27 | "Programming Language :: Python :: 3.11", 28 | "Programming Language :: Python :: 3.12", 29 | "Programming Language :: Python :: 3.13", 30 | "Programming Language :: Python :: 3.14", 31 | ] 32 | dynamic = [ 33 | "version", 34 | ] 35 | dependencies = [ 36 | "pre-commit>=4.3", 37 | "uv>=0.9.1", 38 | ] 39 | optional-dependencies.testing = [ 40 | "covdefaults>=2.3", 41 | "pytest>=8.4.2", 42 | "pytest-cov>=7", 43 | "pytest-mock>=3.15.1", 44 | ] 45 | urls."Bug Tracker" = "https://github.com/tox-dev/pre-commit-uv/issues" 46 | urls."Changelog" = "https://github.com/tox-dev/pre-commit-uv/releases" 47 | urls.Documentation = "https://github.com/tox-dev/pre-commit-uv/" 48 | urls."Source Code" = "https://github.com/tox-dev/pre-commit-uv" 49 | 50 | [tool.hatch] 51 | build.dev-mode-dirs = [ 52 | "src", 53 | ] 54 | build.targets.sdist.include = [ 55 | "/src", 56 | "/tests", 57 | "tox.ini", 58 | ] 59 | build.targets.wheel.only-include = [ "src" ] 60 | build.targets.wheel.sources = [ "src" ] 61 | version.source = "vcs" 62 | 63 | [tool.ruff] 64 | line-length = 120 65 | format.preview = true 66 | format.docstring-code-line-length = 100 67 | format.docstring-code-format = true 68 | lint.select = [ 69 | "ALL", 70 | ] 71 | lint.ignore = [ 72 | "ANN101", # no type annotation for self 73 | "ANN401", # allow Any as type annotation 74 | "COM812", # Conflict with formatter 75 | "CPY", # No copyright statements 76 | "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible 77 | "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible 78 | "DOC501", # not working with Sphinx 79 | "ISC001", # Conflict with formatter 80 | "S104", # Possible binding to all interfaces 81 | ] 82 | lint.per-file-ignores."tests/**/*.py" = [ 83 | "D", # don"t care about documentation in tests 84 | "FBT", # don"t care about booleans as positional arguments in tests 85 | "INP001", # no implicit namespace 86 | "PLC2701", # private import 87 | "PLR0913", # any number of arguments in tests 88 | "PLR0917", # any number of arguments in tests 89 | "PLR2004", # Magic value used in comparison, consider replacing with a constant variable 90 | "S", # `subprocess` call: check for execution of untrusted input 91 | "S101", # asserts allowed in tests... 92 | ] 93 | lint.isort = { known-first-party = [ 94 | "pre_commit_uv", 95 | ], required-imports = [ 96 | "from __future__ import annotations", 97 | ] } 98 | lint.preview = true 99 | 100 | [tool.codespell] 101 | builtin = "clear,usage,en-GB_to_en-US" 102 | count = true 103 | 104 | [tool.pyproject-fmt] 105 | max_supported_python = "3.14" 106 | 107 | [tool.pytest] 108 | ini_options.testpaths = [ 109 | "tests", 110 | ] 111 | 112 | [tool.coverage] 113 | html.show_contexts = true 114 | html.skip_covered = false 115 | paths.source = [ 116 | "src", 117 | ".tox/*/.venv/lib/*/site-packages", 118 | ".tox\\*\\.venv\\Lib\\site-packages", 119 | ".tox/*/lib/*/site-packages", 120 | ".tox\\*\\Lib\\site-packages", 121 | "**/src", 122 | "**\\src", 123 | ] 124 | report.fail_under = 63 125 | run.parallel = true 126 | run.plugins = [ 127 | "covdefaults", 128 | ] 129 | 130 | [tool.mypy] 131 | show_error_codes = true 132 | strict = true 133 | overrides = [ 134 | { module = [ 135 | "pre_commit.*", 136 | ], ignore_missing_imports = true }, 137 | ] 138 | -------------------------------------------------------------------------------- /src/pre_commit_uv/__init__.py: -------------------------------------------------------------------------------- 1 | """Package root.""" 2 | 3 | from __future__ import annotations 4 | 5 | # only import built-ins at top level to avoid interpreter startup overhead 6 | import os 7 | import sys 8 | 9 | _original_main = None 10 | 11 | 12 | def _patch() -> None: 13 | global _original_main 14 | if _original_main is not None: # already patched, nothing more to do 15 | return 16 | calling_pre_commit = "FORCE_PRE_COMMIT_UV_PATCH" in os.environ 17 | if not calling_pre_commit and sys.argv and sys.argv[0]: # must have arguments 18 | calling = sys.argv[1] if sys.argv[0] == sys.executable and len(sys.argv) >= 1 else sys.argv[0] 19 | if ( 20 | os.path.split(calling)[1] == f"pre-commit{'.exe' if sys.platform == 'win32' else ''}" 21 | # case when pre-commit is called due to a git commit 22 | or ("-m" in sys.argv and "hook-impl" in sys.argv) 23 | ): 24 | calling_pre_commit = True 25 | 26 | if calling_pre_commit and os.environ.get("DISABLE_PRE_COMMIT_UV_PATCH") is None: 27 | from pre_commit import main # noqa: PLC0415 28 | 29 | _original_main, main.main = main.main, _new_main 30 | if "--version" in sys.argv: 31 | from importlib.metadata import version as _metadata_version # noqa: PLC0415 32 | 33 | from pre_commit import constants # noqa: PLC0415 34 | 35 | constants.VERSION = ( 36 | f"{constants.VERSION} (" 37 | f"pre-commit-uv={_metadata_version('pre-commit-uv')}, " 38 | f"uv={_metadata_version('uv')}" 39 | f")" 40 | ) 41 | 42 | 43 | def _new_main(argv: list[str] | None = None) -> int: 44 | # imports applied locally to avoid patching import overhead cost 45 | from functools import cache # noqa: PLC0415 46 | from typing import TYPE_CHECKING, cast # noqa: PLC0415 47 | 48 | from pre_commit.languages import python # noqa: PLC0415 49 | 50 | if TYPE_CHECKING: 51 | from collections.abc import Sequence # noqa: PLC0415 52 | 53 | from pre_commit.prefix import Prefix # noqa: PLC0415 54 | 55 | def _install_environment( 56 | prefix: Prefix, 57 | version: str, 58 | additional_dependencies: Sequence[str], 59 | ) -> None: 60 | import logging # noqa: PLC0415 61 | 62 | from pre_commit.git import get_root # noqa: PLC0415 63 | from pre_commit.lang_base import environment_dir, setup_cmd # noqa: PLC0415 64 | from pre_commit.util import cmd_output_b # noqa: PLC0415 65 | 66 | project_root_dir = get_root() 67 | 68 | logger = logging.getLogger("pre_commit") 69 | logger.info("Using pre-commit with uv %s via pre-commit-uv %s", uv_version(), self_version()) 70 | uv = _uv() 71 | py = python.norm_version(version) or os.environ.get("UV_PYTHON", sys.executable) 72 | venv_cmd = [ 73 | uv, 74 | "--project", 75 | project_root_dir, 76 | "venv", 77 | environment_dir(prefix, python.ENVIRONMENT_DIR, version), 78 | "-p", 79 | py, 80 | ] 81 | cmd_output_b(*venv_cmd, cwd="/") 82 | 83 | with python.in_env(prefix, version): 84 | setup_cmd( 85 | prefix, 86 | ( 87 | uv, 88 | "--project", 89 | project_root_dir, 90 | "pip", 91 | "install", 92 | ".", 93 | *additional_dependencies, 94 | ), 95 | ) 96 | 97 | @cache 98 | def _uv() -> str: 99 | from uv import find_uv_bin # noqa: PLC0415 100 | 101 | return find_uv_bin() 102 | 103 | @cache 104 | def self_version() -> str: 105 | from importlib.metadata import version as _metadata_version # noqa: PLC0415 106 | 107 | return _metadata_version("pre-commit-uv") 108 | 109 | @cache 110 | def uv_version() -> str: 111 | from importlib.metadata import version as _metadata_version # noqa: PLC0415 112 | 113 | return _metadata_version("uv") 114 | 115 | @cache 116 | def _version_info(exe: str) -> str: 117 | from pre_commit.util import CalledProcessError, cmd_output # noqa: PLC0415 118 | 119 | prog = 'import sys;print(".".join(str(p) for p in sys.version_info[0:3]))' 120 | try: 121 | return cast("str", cmd_output(exe, "-S", "-c", prog)[1].strip()) 122 | except CalledProcessError: 123 | return f"<>" 124 | 125 | python.install_environment = _install_environment 126 | python._version_info = _version_info # noqa: SLF001 127 | assert _original_main is not None # noqa: S101 128 | return cast("int", _original_main(argv)) 129 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from importlib.metadata import version 4 | from subprocess import check_call, check_output 5 | from textwrap import dedent 6 | from typing import TYPE_CHECKING 7 | 8 | import pytest 9 | from pre_commit import main 10 | 11 | if TYPE_CHECKING: 12 | from pathlib import Path 13 | 14 | precommit_file = ".pre-commit-config.yaml" 15 | uv = version("uv") 16 | self = version("pre-commit-uv") 17 | 18 | 19 | @pytest.fixture 20 | def git_repo(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: 21 | conf = """ 22 | repos: 23 | - repo: https://github.com/tox-dev/pyproject-fmt 24 | rev: "2.2.0" 25 | hooks: 26 | - id: pyproject-fmt 27 | """ 28 | conf_file = tmp_path / precommit_file 29 | conf_file.write_text(dedent(conf)) 30 | monkeypatch.setenv("PRE_COMMIT_HOME", str(tmp_path / "store")) 31 | monkeypatch.chdir(tmp_path) 32 | check_call(["git", "init"]) 33 | return tmp_path 34 | 35 | 36 | @pytest.fixture 37 | def install_hook(git_repo: Path) -> None: 38 | check_call(["pre-commit", "install", "--install-hooks", "-c", str(git_repo / precommit_file)]) 39 | check_call(["pre-commit", "clean"]) # ensures that 'install_environment' gets called 40 | 41 | 42 | @pytest.mark.usefixtures("install_hook") 43 | def test_run_precommit_hook() -> None: 44 | hook_result = check_output([".git/hooks/pre-commit"], encoding="utf-8") 45 | assert f"[INFO] Using pre-commit with uv {uv} via pre-commit-uv {self}" in hook_result.splitlines() 46 | 47 | 48 | @pytest.mark.usefixtures("install_hook") 49 | def test_call_as_module() -> None: 50 | run_result = check_output(["python3", "-m", "pre_commit", "run", "-a", "--color", "never"], encoding="utf-8") 51 | assert f"[INFO] Using pre-commit with uv {uv} via pre-commit-uv {self}" not in run_result.splitlines() 52 | 53 | 54 | def test_install(git_repo: Path, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch) -> None: 55 | monkeypatch.setenv("FORCE_PRE_COMMIT_UV_PATCH", "1") 56 | 57 | import pre_commit_uv # noqa: PLC0415 58 | 59 | pre_commit_uv._patch() # noqa: SLF001 60 | main.main(["install-hooks", "-c", str(git_repo / precommit_file)]) 61 | 62 | assert caplog.messages == [ 63 | "Initializing environment for https://github.com/tox-dev/pyproject-fmt.", 64 | "Installing environment for https://github.com/tox-dev/pyproject-fmt.", 65 | "Once installed this environment will be reused.", 66 | "This may take a few minutes...", 67 | f"Using pre-commit with uv {uv} via pre-commit-uv {self}", 68 | ] 69 | 70 | 71 | test_install_with_uv_config_cases: list[tuple[str, str]] = [ 72 | ( 73 | "pyproject.toml", 74 | """ 75 | [[tool.uv.index]] 76 | name = "internal" 77 | url = "https://pypi.org/simple/" 78 | default = true 79 | """, 80 | ), 81 | ( 82 | "uv.toml", 83 | """ 84 | [[index]] 85 | name = "internal" 86 | url = "https://pypi.org/simple/" 87 | default = true 88 | """, 89 | ), 90 | ] 91 | 92 | 93 | @pytest.mark.parametrize( 94 | ("file_name", "content"), 95 | test_install_with_uv_config_cases, 96 | ) 97 | def test_install_with_uv_config( 98 | git_repo: Path, 99 | caplog: pytest.LogCaptureFixture, 100 | monkeypatch: pytest.MonkeyPatch, 101 | file_name: str, 102 | content: str, 103 | ) -> None: 104 | (git_repo / file_name).write_text(dedent(content)) 105 | 106 | monkeypatch.setenv("FORCE_PRE_COMMIT_UV_PATCH", "1") 107 | 108 | import pre_commit_uv # noqa: PLC0415 109 | 110 | pre_commit_uv._patch() # noqa: SLF001 111 | main.main(["install-hooks", "-c", str(git_repo / precommit_file)]) 112 | 113 | assert caplog.messages == [ 114 | "Initializing environment for https://github.com/tox-dev/pyproject-fmt.", 115 | "Installing environment for https://github.com/tox-dev/pyproject-fmt.", 116 | "Once installed this environment will be reused.", 117 | "This may take a few minutes...", 118 | f"Using pre-commit with uv {uv} via pre-commit-uv {self}", 119 | ] 120 | 121 | 122 | test_install_with_uv_config_raises_error_cases: list[tuple[str, str]] = [ 123 | ( 124 | "pyproject.toml", 125 | """ 126 | [[tool.uv.index]] 127 | name = "internal" 128 | url = "https://pypi.example/simple/" 129 | default = true 130 | """, 131 | ), 132 | ( 133 | "uv.toml", 134 | """ 135 | [[index]] 136 | name = "internal" 137 | url = "https://pypi.example/simple/" 138 | default = true 139 | """, 140 | ), 141 | ] 142 | 143 | 144 | @pytest.mark.parametrize(("file_name", "content"), test_install_with_uv_config_raises_error_cases) 145 | def test_install_with_uv_config_raises_error( 146 | git_repo: Path, 147 | monkeypatch: pytest.MonkeyPatch, 148 | file_name: str, 149 | content: str, 150 | ) -> None: 151 | """Test to make sure that uv config is used for non default pypi repos.""" 152 | (git_repo / file_name).write_text(dedent(content)) 153 | 154 | monkeypatch.setenv("FORCE_PRE_COMMIT_UV_PATCH", "1") 155 | 156 | import pre_commit_uv # noqa: PLC0415 157 | 158 | pre_commit_uv._patch() # noqa: SLF001 159 | 160 | # would raise SystemExit due to bad config 161 | with pytest.raises(SystemExit): 162 | main.main(["install-hooks", "-c", str(git_repo / precommit_file)]) 163 | --------------------------------------------------------------------------------