Skip to content

Commit 0f0dcbe

Browse files
authored
feat(prek): support prek as hook runner (#73)
* feat(prek): support `prek` as hook runner
1 parent 4966c00 commit 0f0dcbe

File tree

9 files changed

+295
-93
lines changed

9 files changed

+295
-93
lines changed

README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ ignore = []
9090
# Name of the pre-commit config file to sync with
9191
# Can be set to ".pre-commit-config.yml" to support prek alternate config file
9292
pre-commit-config-file = ".pre-commit-config.yaml"
93+
# Hook runner to use for installing hooks: "pre-commit", "prek", or "auto"
94+
# "auto" tries prek first, then falls back to pre-commit
95+
hook-runner = "pre-commit"
9396
# Additional mapping of URLs to python packages
9497
# Default is empty, but will merge with the default mapping
9598
# "rev" indicates the format of the Git tags
@@ -103,12 +106,13 @@ dependency-mapping = {"package-name"= {"repo"= "https://github.com/example/packa
103106

104107
Some settings are overridable by environment variables with the following `SYNC_PRE_COMMIT_LOCK_*` prefixed environment variables:
105108

106-
| `toml` setting | environment | format |
107-
| ----------------------------- | -------------------------------------- | --------------------------------- |
108-
| `automatically-install-hooks` | `SYNC_PRE_COMMIT_LOCK_INSTALL` | `bool` as string (`true`, `1`...) |
109-
| `disable-sync-from-lock` | `SYNC_PRE_COMMIT_LOCK_DISABLED` | `bool` as string (`true`, `1`...) |
110-
| `ignore` | `SYNC_PRE_COMMIT_LOCK_IGNORE` | comma-separated list |
111-
| `pre-commit-config-file` | `SYNC_PRE_COMMIT_LOCK_PRE_COMMIT_FILE` | `str` |
109+
| `toml` setting | environment | format |
110+
| ----------------------------- | -------------------------------------- | ------------------------------------------- |
111+
| `automatically-install-hooks` | `SYNC_PRE_COMMIT_LOCK_INSTALL` | `bool` as string (`true`, `1`...) |
112+
| `disable-sync-from-lock` | `SYNC_PRE_COMMIT_LOCK_DISABLED` | `bool` as string (`true`, `1`...) |
113+
| `ignore` | `SYNC_PRE_COMMIT_LOCK_IGNORE` | comma-separated list |
114+
| `pre-commit-config-file` | `SYNC_PRE_COMMIT_LOCK_PRE_COMMIT_FILE` | `str` |
115+
| `hook-runner` | `SYNC_PRE_COMMIT_LOCK_HOOK_RUNNER` | `str` (`pre-commit`, `prek`, or `auto`) |
112116

113117
## Usage
114118

src/sync_pre_commit_lock/actions/install_hooks.py

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,75 @@
77
from pathlib import Path
88
from typing import TYPE_CHECKING, ClassVar
99

10+
from sync_pre_commit_lock.config import HookRunner
11+
1012
if TYPE_CHECKING:
1113
from collections.abc import Sequence
1214

1315
from sync_pre_commit_lock import Printer
1416

1517

16-
class SetupPreCommitHooks:
17-
install_pre_commit_hooks_command: ClassVar[Sequence[str | bytes]] = ["pre-commit", "install"]
18-
check_pre_commit_version_command: ClassVar[Sequence[str | bytes]] = ["pre-commit", "--version"]
18+
class ResolvedHookRunner:
19+
"""A resolved hook runner, bound to a concrete runner (never AUTO) and a command prefix."""
20+
21+
# Probe order for auto-detection: try prek first, then pre-commit
22+
_AUTO_ORDER: ClassVar[tuple[HookRunner, ...]] = (HookRunner.PREK, HookRunner.PRE_COMMIT)
23+
24+
def __init__(self, runner: HookRunner, command_prefix: Sequence[str] = ()) -> None:
25+
self.runner = runner
26+
self.command_prefix = command_prefix
27+
28+
@property
29+
def name(self) -> str:
30+
return self.runner.value
31+
32+
def execute(self, *args: str) -> Sequence[str | bytes]:
33+
return [*self.command_prefix, self.runner.value, *args]
34+
35+
def is_installed(self) -> bool:
36+
"""Check if this runner is installed by running its --version command."""
37+
try:
38+
output = subprocess.check_output(self.execute("--version")).decode() # noqa: S603
39+
except (subprocess.CalledProcessError, FileNotFoundError):
40+
return False
41+
else:
42+
return self.runner.value in output
43+
44+
@classmethod
45+
def resolve(
46+
cls,
47+
hook_runner: HookRunner,
48+
command_prefix: Sequence[str] = (),
49+
printer: Printer | None = None,
50+
) -> ResolvedHookRunner | None:
51+
"""Resolve a HookRunner config to a concrete ResolvedHookRunner, or None if not found."""
52+
candidates = cls._AUTO_ORDER if hook_runner is HookRunner.AUTO else [hook_runner]
53+
for candidate in candidates:
54+
runner = cls(candidate, command_prefix)
55+
if runner.is_installed():
56+
if hook_runner is HookRunner.AUTO and printer:
57+
printer.debug(f"Auto-detected hook runner: {candidate.value}")
58+
return runner
59+
return None
60+
1961

20-
def __init__(self, printer: Printer, dry_run: bool = False) -> None:
62+
class SetupPreCommitHooks:
63+
command_prefix: ClassVar[Sequence[str]] = ()
64+
65+
def __init__(
66+
self,
67+
printer: Printer,
68+
dry_run: bool = False,
69+
hook_runner: HookRunner = HookRunner.PRE_COMMIT,
70+
) -> None:
2171
self.printer = printer
2272
self.dry_run = dry_run
73+
self.hook_runner = hook_runner
2374

2475
def execute(self) -> None:
25-
if not self._is_pre_commit_package_installed():
26-
self.printer.debug("pre-commit package is not installed (or detected). Skipping.")
76+
runner = ResolvedHookRunner.resolve(self.hook_runner, self.command_prefix, self.printer)
77+
if runner is None:
78+
self.printer.debug("No hook runner (pre-commit or prek) is installed (or detected). Skipping.")
2779
return
2880

2981
git_root = self._get_git_directory_path()
@@ -39,36 +91,25 @@ def execute(self) -> None:
3991
self.printer.debug("Dry run, skipping pre-commit hook installation.")
4092
return
4193

42-
self._install_pre_commit_hooks()
94+
self._install_hooks(runner)
4395

44-
def _install_pre_commit_hooks(self) -> None:
96+
def _install_hooks(self, runner: ResolvedHookRunner) -> None:
4597
try:
46-
self.printer.info("Installing pre-commit hooks...")
98+
self.printer.info(f"Installing {runner.name} hooks...")
4799
return_code = subprocess.check_call( # noqa: S603
48-
self.install_pre_commit_hooks_command,
100+
runner.execute("install"),
49101
# XXX We probably want to see the output, at least in verbose mode or if it fails
50102
stdout=subprocess.DEVNULL,
51103
stderr=subprocess.DEVNULL,
52104
)
53105
if return_code == 0:
54-
self.printer.info("pre-commit hooks successfully installed!")
106+
self.printer.info(f"{runner.name} hooks successfully installed!")
55107
else:
56-
self.printer.error("Failed to install pre-commit hooks")
108+
self.printer.error(f"Failed to install {runner.name} hooks")
57109
except Exception as e:
58-
self.printer.error("Failed to install pre-commit hooks due to an unexpected error")
110+
self.printer.error(f"Failed to install {runner.name} hooks due to an unexpected error")
59111
self.printer.error(f"{e}")
60112

61-
def _is_pre_commit_package_installed(self) -> bool:
62-
try:
63-
# Try is `pre-commit --version` works
64-
output = subprocess.check_output( # noqa: S603
65-
self.check_pre_commit_version_command,
66-
).decode()
67-
except (subprocess.CalledProcessError, FileNotFoundError):
68-
return False
69-
else:
70-
return "pre-commit" in output
71-
72113
@staticmethod
73114
def _are_pre_commit_hooks_installed(git_root: Path) -> bool:
74115
return (git_root / "hooks" / "pre-commit").exists()

src/sync_pre_commit_lock/config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import os
44
from dataclasses import dataclass, field
5+
from enum import Enum
56
from pathlib import Path
67
from typing import TYPE_CHECKING, Any, Callable, TypedDict
78

@@ -10,6 +11,15 @@
1011
if TYPE_CHECKING:
1112
from sync_pre_commit_lock.db import PackageRepoMapping
1213

14+
15+
class HookRunner(str, Enum):
16+
"""The hook runner to use for installing git hooks."""
17+
18+
PRE_COMMIT = "pre-commit"
19+
PREK = "prek"
20+
AUTO = "auto"
21+
22+
1323
ENV_PREFIX = "SYNC_PRE_COMMIT_LOCK"
1424

1525

@@ -74,6 +84,10 @@ class SyncPreCommitLockConfig:
7484
default_factory=dict,
7585
metadata=Metadata(toml="dependency-mapping"),
7686
)
87+
hook_runner: HookRunner = field(
88+
default=HookRunner.PRE_COMMIT,
89+
metadata=Metadata(toml="hook-runner", env="HOOK_RUNNER", cast=HookRunner),
90+
)
7791

7892

7993
def load_config(path: Path | None = None) -> SyncPreCommitLockConfig:

src/sync_pre_commit_lock/pdm_plugin.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,7 @@ def register_pdm_plugin(core: Core) -> None:
132132

133133

134134
class PDMSetupPreCommitHooks(SetupPreCommitHooks):
135-
install_pre_commit_hooks_command: ClassVar[Sequence[str | bytes]] = ["pdm", "run", "pre-commit", "install"]
136-
check_pre_commit_version_command: ClassVar[Sequence[str | bytes]] = ["pdm", "run", "pre-commit", "--version"]
135+
command_prefix: ClassVar[Sequence[str]] = ("pdm", "run")
137136

138137

139138
class PDMSyncPreCommitHooksVersion(SyncPreCommitHooksVersion):
@@ -150,7 +149,7 @@ def on_pdm_install_setup_pre_commit(project: Project, *, dry_run: bool, **_: Any
150149
if not plugin_config.automatically_install_hooks:
151150
printer.debug("Automatically installing pre-commit hooks is disabled. Skipping.")
152151
return
153-
action = PDMSetupPreCommitHooks(printer, dry_run=dry_run)
152+
action = PDMSetupPreCommitHooks(printer, dry_run=dry_run, hook_runner=plugin_config.hook_runner)
154153
file_path = project.root / plugin_config.pre_commit_config_file
155154
if not file_path.exists():
156155
printer.info("No pre-commit config file found, skipping pre-commit hook check")

src/sync_pre_commit_lock/poetry_plugin.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,7 @@ def _format_additional_dependency(self, old: str, new: str, prefix: str, last: b
136136

137137

138138
class PoetrySetupPreCommitHooks(SetupPreCommitHooks):
139-
install_pre_commit_hooks_command: ClassVar[Sequence[str | bytes]] = ["poetry", "run", "pre-commit", "install"]
140-
check_pre_commit_version_command: ClassVar[Sequence[str | bytes]] = ["poetry", "run", "pre-commit", "--version"]
139+
command_prefix: ClassVar[Sequence[str]] = ("poetry", "run")
141140

142141

143142
def run_sync_pre_commit_version(printer: PoetryPrinter, dry_run: bool, application: Application) -> None:
@@ -186,7 +185,8 @@ def _handle_post_command(
186185
return
187186

188187
if any(isinstance(command, t) for t in [InstallCommand, AddCommand]):
189-
PoetrySetupPreCommitHooks(printer, dry_run=dry_run).execute()
188+
plugin_config = load_config(self.application.poetry.pyproject_path) if self.application else load_config()
189+
PoetrySetupPreCommitHooks(printer, dry_run=dry_run, hook_runner=plugin_config.hook_runner).execute()
190190

191191
if any(isinstance(command, t) for t in [InstallCommand, AddCommand, LockCommand, UpdateCommand]):
192192
if self.application is None:

0 commit comments

Comments
 (0)