Skip to content

Commit db00603

Browse files
authored
Avoid patching import overheads and extend README (#2)
1 parent 4727fb8 commit db00603

File tree

9 files changed

+157
-110
lines changed

9 files changed

+157
-110
lines changed

.github/SECURITY.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
## Supported Versions
44

55
| Version | Supported |
6-
|---------| ------------------ |
7-
| 0.1 + | :white_check_mark: |
8-
| < 0.1 | :x: |
6+
| ------- | ------------------ |
7+
| 4+ | :white_check_mark: |
8+
| <4 | :x: |
99

1010
## Reporting a Vulnerability
1111

.github/workflows/check.yml

Lines changed: 21 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,64 +14,41 @@ concurrency:
1414

1515
jobs:
1616
test:
17-
name: test ${{ matrix.py }}
18-
runs-on: ubuntu-latest
17+
name: Run ${{ matrix.tox_env }} on ${{ matrix.os }}
18+
runs-on: ${{ matrix.os }}
1919
strategy:
2020
fail-fast: false
2121
matrix:
22-
py:
22+
tox_env:
2323
- "3.12"
2424
- "3.11"
2525
- "3.10"
2626
- "3.9"
27-
steps:
28-
- name: setup uv for tox
29-
uses: yezz123/setup-uv@v4
30-
- name: setup python for tox
31-
uses: actions/setup-python@v5
32-
with:
33-
python-version: "3.12"
34-
- name: install tox
35-
run: uv pip install tox tox-uv --system
36-
- uses: actions/checkout@v4
37-
- name: setup python for test ${{ matrix.py }}
38-
uses: actions/setup-python@v5
39-
with:
40-
python-version: ${{ matrix.py }}
41-
- name: Pick environment to run
42-
run: |
43-
import codecs; import os; import sys
44-
env = "TOXENV=py{}{}\n".format(*sys.version_info[0:2])
45-
print("Picked:\n{}for{}".format(env, sys.version))
46-
with codecs.open(os.environ["GITHUB_ENV"], "a", "utf-8") as file_handler:
47-
file_handler.write(env)
48-
shell: python
49-
- name: setup test suite
50-
run: tox -vv --notest
51-
- name: run test suite
52-
run: tox --skip-pkg-install
53-
54-
check:
55-
name: tox env ${{ matrix.tox_env }}
56-
runs-on: ubuntu-latest
57-
strategy:
58-
fail-fast: false
59-
matrix:
60-
tox_env:
6127
- type
6228
- dev
6329
- readme
30+
os:
31+
- ubuntu-latest
32+
- windows-latest
33+
- macos-latest
6434
steps:
65-
- name: setup uv for tox
35+
- name: Set up uv for tox
6636
uses: yezz123/setup-uv@v4
67-
- uses: actions/checkout@v4
68-
- name: setup Python 3.12
37+
- name: Set up python for tox
6938
uses: actions/setup-python@v5
7039
with:
7140
python-version: "3.12"
72-
- name: install tox
41+
- name: Install tox-uv
7342
run: uv pip install tox tox-uv --system
74-
- name: run check for ${{ matrix.tox_env }}
75-
run: python -m tox -e ${{ matrix.tox_env }}
43+
- name: Checkout source code
44+
uses: actions/checkout@v4
45+
- name: Set up ${{ startsWith(matrix.tox_env, '3.') && matrix.tox_env || '3.12' }} for test
46+
uses: actions/setup-python@v5
47+
with:
48+
python-version: ${{ startsWith(matrix.tox_env, '3.') && matrix.tox_env || '3.12' }}
49+
- name: Set up test environment
50+
run: tox -vv -e ${{ matrix.tox_env }} --notest
51+
- name: Run test environment
52+
run: tox -e ${{ matrix.tox_env }} --skip-pkg-install
7653
env:
77-
UPGRADE_ADVISORY: "yes"
54+
PYTEST_ADDOPTS: "-vv --showlocals"

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ repos:
88
rev: 0.29.1
99
hooks:
1010
- id: check-github-workflows
11-
args: [ "--verbose" ]
11+
args: ["--verbose"]
1212
- repo: https://github.com/codespell-project/codespell
1313
rev: v2.3.0
1414
hooks:

README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,33 @@
99

1010
Use `uv` to create virtual environments and install packages for `pre-commit`.
1111

12+
## Why?
13+
14+
Compared to upstream `pre-commit` will speed up the initial seed operation. In general, upstream recommends caching the
15+
`pre-commit` cache, however, that is not always possible and is still helpful to have a more performant initial cache
16+
creation., Here's an example of what you could expect demonstrated on this project's own pre-commit setup (with a hot
17+
`uv` cache):
18+
19+
```shell
20+
❯ hyperfine 'pre-commit install-hooks' 'pre-commit-uv install-hooks'
21+
Benchmark 1: pre-commit install-hooks
22+
Time (mean ± σ): 54.132 s ± 8.827 s [User: 15.424 s, System: 9.359 s]
23+
Range (min … max): 45.972 s … 66.506 s 10 runs
24+
25+
Benchmark 2: pre-commit-uv install-hooks
26+
Time (mean ± σ): 41.695 s ± 7.395 s [User: 7.614 s, System: 6.133 s]
27+
Range (min … max): 32.198 s … 58.467 s 10 runs
28+
29+
Summary
30+
pre-commit-uv install-hooks ran 1.30 ± 0.31 times faster than pre-commit install-hooks
31+
```
32+
1233
## Configuration
1334

14-
Once installed will use `uv` out of box, however the `DISABLE_PRE_COMMIT_UV_PATCH` environment variable if set will work as an escape hatch.
35+
Once installed will use `uv` out of box, however the `DISABLE_PRE_COMMIT_UV_PATCH` environment variable if is set it
36+
will work as an escape hatch to disable the new behavior.
37+
38+
To avoid interpreter startup overhead of the patching, we only perform this when we detect you calling `pre-commit`.
39+
Should this logic fail you can force the patching by setting the `FORCE_PRE_COMMIT_UV_PATCH` variable. Should you
40+
experience this please raise an issue with the content of the `sys.argv`. Note that `DISABLE_PRE_COMMIT_UV_PATCH` will
41+
overwrite this flag should both be set.

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ build.targets.sdist.include = [
5555
"/tests",
5656
"tox.ini",
5757
]
58-
build.targets.wheel.force-include = { "src/pre_commit_uv_patch.pth" = "pre_commit_uv_patch.pth" }
58+
build.targets.wheel.only-include = [ "src" ]
59+
build.targets.wheel.sources = [ "src" ]
5960
version.source = "vcs"
6061

6162
[tool.ruff]
@@ -86,8 +87,8 @@ lint.per-file-ignores."tests/**/*.py" = [
8687
"PLR0913", # any number of arguments in tests
8788
"PLR0917", # any number of arguments in tests
8889
"PLR2004", # Magic value used in comparison, consider replacing with a constant variable
90+
"S", # `subprocess` call: check for execution of untrusted input
8991
"S101", # asserts allowed in tests...
90-
"S603", # `subprocess` call: check for execution of untrusted input
9192
]
9293
lint.isort = { known-first-party = [
9394
"pre_commit_uv",
@@ -117,7 +118,7 @@ paths.source = [
117118
"**/src",
118119
"**\\src",
119120
]
120-
report.fail_under = 50
121+
report.fail_under = 65
121122
run.parallel = true
122123
run.plugins = [
123124
"covdefaults",

src/pre_commit_uv/__init__.py

Lines changed: 71 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,67 +2,92 @@
22

33
from __future__ import annotations
44

5-
import logging
5+
# only import built-ins at top level to avoid interpreter startup overhead
66
import os
7-
from functools import cache
8-
from importlib.metadata import version as _metadata_version
9-
from typing import TYPE_CHECKING, cast
7+
import sys
108

11-
from pre_commit import lang_base, main
12-
from pre_commit.languages import python
13-
from pre_commit.languages.python import in_env, norm_version
14-
from pre_commit.util import CalledProcessError, cmd_output, cmd_output_b
15-
from uv import find_uv_bin
9+
_original_main = None
1610

17-
if TYPE_CHECKING:
18-
from collections.abc import Sequence
1911

20-
from pre_commit.prefix import Prefix
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 os.path.split(calling)[1] == f"pre-commit{'.exe' if sys.platform == 'win32' else ''}":
20+
calling_pre_commit = True
2121

22+
if calling_pre_commit and os.environ.get("DISABLE_PRE_COMMIT_UV_PATCH") is None:
23+
from pre_commit import main # noqa: PLC0415
2224

23-
__version__ = _metadata_version("pre-commit-uv")
24-
_uv_version = _metadata_version("uv")
25-
_original_main = main.main
25+
_original_main, main.main = main.main, _new_main
2626

2727

28-
def _patch() -> None:
29-
if os.environ.get("DISABLE_PRE_COMMIT_UV_PATCH") is None:
30-
main.main = _new_main
28+
def _new_main(argv: list[str] | None = None) -> int:
29+
# imports applied locally to avoid patching import overhead cost
30+
from functools import cache # noqa: PLC0415
31+
from typing import TYPE_CHECKING, cast # noqa: PLC0415
3132

33+
from pre_commit.languages import python # noqa: PLC0415
3234

33-
def _new_main(argv: Sequence[str] | None = None) -> int:
34-
python.install_environment = _install_environment
35-
python._version_info = _version_info # noqa: SLF001
36-
return cast(int, _original_main(argv))
35+
if TYPE_CHECKING:
36+
from collections.abc import Sequence # noqa: PLC0415
37+
38+
from pre_commit.prefix import Prefix # noqa: PLC0415
39+
40+
def _install_environment(
41+
prefix: Prefix,
42+
version: str,
43+
additional_dependencies: Sequence[str],
44+
) -> None:
45+
import logging # noqa: PLC0415
3746

47+
from pre_commit.lang_base import environment_dir, setup_cmd # noqa: PLC0415
48+
from pre_commit.util import cmd_output_b # noqa: PLC0415
3849

39-
def _install_environment(
40-
prefix: Prefix,
41-
version: str,
42-
additional_dependencies: Sequence[str],
43-
) -> None:
44-
logging.getLogger("pre_commit").info("Using pre-commit with uv %s via pre-commit-uv %s", _uv_version, __version__)
45-
uv = find_uv_bin()
50+
logger = logging.getLogger("pre_commit")
51+
logger.info("Using pre-commit with uv %s via pre-commit-uv %s", uv_version(), self_version())
52+
uv = _uv()
53+
venv_cmd = [uv, "venv", environment_dir(prefix, python.ENVIRONMENT_DIR, version)]
54+
py = python.norm_version(version)
55+
if py is not None:
56+
venv_cmd.extend(("-p", py))
57+
cmd_output_b(*venv_cmd, cwd="/")
4658

47-
venv_cmd = [uv, "venv", lang_base.environment_dir(prefix, python.ENVIRONMENT_DIR, version)]
48-
py = norm_version(version)
49-
if py is not None:
50-
venv_cmd.extend(("-p", py))
51-
cmd_output_b(*venv_cmd, cwd="/")
59+
with python.in_env(prefix, version):
60+
setup_cmd(prefix, (uv, "pip", "install", ".", *additional_dependencies))
5261

53-
with in_env(prefix, version):
54-
lang_base.setup_cmd(prefix, (uv, "pip", "install", ".", *additional_dependencies))
62+
@cache
63+
def _uv() -> str:
64+
from uv import find_uv_bin # noqa: PLC0415
5565

66+
return find_uv_bin()
5667

57-
@cache
58-
def _version_info(exe: str) -> str:
59-
prog = 'import sys;print(".".join(str(p) for p in sys.version_info[0:3]))'
60-
try:
61-
return cast(str, cmd_output(exe, "-S", "-c", prog)[1].strip())
62-
except CalledProcessError:
63-
return f"<<error retrieving version from {exe}>>"
68+
@cache
69+
def self_version() -> str:
70+
from importlib.metadata import version as _metadata_version # noqa: PLC0415
6471

72+
return _metadata_version("pre-commit-uv")
6573

66-
__all__ = [
67-
"__version__",
68-
]
74+
@cache
75+
def uv_version() -> str:
76+
from importlib.metadata import version as _metadata_version # noqa: PLC0415
77+
78+
return _metadata_version("uv")
79+
80+
@cache
81+
def _version_info(exe: str) -> str:
82+
from pre_commit.util import CalledProcessError, cmd_output # noqa: PLC0415
83+
84+
prog = 'import sys;print(".".join(str(p) for p in sys.version_info[0:3]))'
85+
try:
86+
return cast(str, cmd_output(exe, "-S", "-c", prog)[1].strip())
87+
except CalledProcessError:
88+
return f"<<error retrieving version from {exe}>>"
89+
90+
python.install_environment = _install_environment
91+
python._version_info = _version_info # noqa: SLF001
92+
assert _original_main is not None # noqa: S101
93+
return cast(int, _original_main(argv))

task/dev_pth.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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])

tests/test_main.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from importlib.metadata import version
4+
from subprocess import check_call
45
from textwrap import dedent
56
from typing import TYPE_CHECKING
67

@@ -23,6 +24,8 @@ def test_install(tmp_path: Path, caplog: pytest.LogCaptureFixture, monkeypatch:
2324
conf_file = tmp_path / ".pre-commit-config.yaml"
2425
conf_file.write_text(dedent(conf))
2526
monkeypatch.setenv("PRE_COMMIT_HOME", str(tmp_path / "store"))
27+
monkeypatch.chdir(tmp_path)
28+
check_call(["git", "init"])
2629

2730
main.main(["install-hooks", "-c", str(conf_file)])
2831

0 commit comments

Comments
 (0)