forked from pre-commit/pre-commit
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This patch adds 2nd class support for hooks using julia as the language. pre-commit will install any dependencies defined in the hooks repo `Project.toml` file, with support for `additional_dependencies` as well. Julia doesn't (yet) have a way to install binaries/scripts so for julia hooks the `entry` value is a (relative) path to a julia script within the hooks repository. When executing a julia hook the (globally installed) julia interpreter is prepended to the entry. Example `.pre-commit-hooks.yaml`: ```yaml - id: foo name: ... language: julia entry: bin/foo.jl --arg1 ``` Example hooks repo: https://github.com/fredrikekre/runic-pre-commit/tree/fe/julia Accompanying pre-commit.com PR: pre-commit/pre-commit.com#998 Fixes pre-commit#2689.
- Loading branch information
1 parent
9da45a6
commit 54d9104
Showing
3 changed files
with
190 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
from __future__ import annotations | ||
|
||
import contextlib | ||
from collections.abc import Generator | ||
from collections.abc import Sequence | ||
|
||
from pre_commit import lang_base | ||
from pre_commit.envcontext import envcontext | ||
from pre_commit.envcontext import PatchesT | ||
from pre_commit.prefix import Prefix | ||
from pre_commit.util import cmd_output_b | ||
|
||
ENVIRONMENT_DIR = 'juliaenv' | ||
health_check = lang_base.basic_health_check | ||
get_default_version = lang_base.basic_get_default_version | ||
|
||
|
||
def run_hook( | ||
prefix: Prefix, | ||
entry: str, | ||
args: Sequence[str], | ||
file_args: Sequence[str], | ||
*, | ||
is_local: bool, | ||
require_serial: bool, | ||
color: bool, | ||
) -> tuple[int, bytes]: | ||
# `entry` is a (hook-repo relative) file followed by (optional) args, e.g. | ||
# `bin/id.jl` or `bin/hook.jl --arg1 --arg2` so we | ||
# 1) shell parse it and join with args with hook_cmd | ||
# 2) prepend the hooks prefix path to the first argument (the file) | ||
# 3) prepend `julia` as the interpreter | ||
cmd = lang_base.hook_cmd(entry, args) | ||
cmd = ('julia', prefix.path(cmd[0]), *cmd[1:]) | ||
return lang_base.run_xargs( | ||
cmd, | ||
file_args, | ||
require_serial=require_serial, | ||
color=color, | ||
) | ||
|
||
|
||
def get_env_patch(target_dir: str, version: str) -> PatchesT: | ||
return ( | ||
# Single entry pointing to the hook env | ||
('JULIA_LOAD_PATH', target_dir), | ||
# May be set, remove it to not interfer with LOAD_PATH | ||
('JULIA_PROJECT', ''), | ||
) | ||
|
||
|
||
@contextlib.contextmanager | ||
def in_env(prefix: Prefix, version: str) -> Generator[None]: | ||
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) | ||
with envcontext(get_env_patch(envdir, version)): | ||
yield | ||
|
||
|
||
def install_environment( | ||
prefix: Prefix, | ||
version: str, | ||
additional_dependencies: Sequence[str], | ||
) -> None: | ||
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version) | ||
with contextlib.ExitStack() as ctx: | ||
ctx.enter_context(in_env(prefix, version)) | ||
|
||
# TODO: Support language_version with juliaup similar to rust via | ||
# rustup | ||
# if version != 'system': | ||
# ... | ||
|
||
# Julia code to setup and instantiate the hook environment | ||
# TODO: This would be easier to read and work with if it can be put in | ||
# a .jl file instead. | ||
julia_code = """ | ||
@assert length(ARGS) > 0 | ||
hook_env = ARGS[1] | ||
deps = join(ARGS[2:end], " ") | ||
# Copy Project.toml to hook env | ||
mkdir(hook_env) | ||
project_names = ("JuliaProject.toml", "Project.toml") | ||
project_found = false | ||
for project_name in project_names | ||
isfile(project_name) || continue | ||
cp(project_name, joinpath(hook_env, project_name)) | ||
global project_found = true | ||
break | ||
end | ||
if !project_found | ||
error("No (Julia)Project.toml found in hooks repository") | ||
end | ||
# Copy Manifest.toml to hook env (not mandatory) | ||
manifest_names = ("JuliaManifest.toml", "Manifest.toml") | ||
for manifest_name in manifest_names | ||
isfile(manifest_name) || continue | ||
cp(manifest_name, joinpath(hook_env, manifest_name)) | ||
break | ||
end | ||
# We prepend @stdlib here so that we can load the package manager even | ||
# though `get_env_patch` limits `JULIA_LOAD_PATH` to just the hook env. | ||
pushfirst!(LOAD_PATH, "@stdlib") | ||
using Pkg | ||
popfirst!(LOAD_PATH) | ||
# Instantiate the environment shipped with the hook repo. If we have | ||
# additional dependencies we disable precompilation in this step to | ||
# avoid double work. | ||
precompile = isempty(deps) ? "1" : "0" | ||
withenv("JULIA_PKG_PRECOMPILE_AUTO" => precompile) do | ||
Pkg.instantiate() | ||
end | ||
# Add additional dependencies (with precompilation) | ||
if !isempty(deps) | ||
withenv("JULIA_PKG_PRECOMPILE_AUTO" => "1") do | ||
Pkg.REPLMode.pkgstr("add " * deps) | ||
end | ||
end | ||
""" | ||
cmd_output_b( | ||
'julia', '-e', julia_code, '--', envdir, *additional_dependencies, | ||
cwd=prefix.prefix_dir, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
from __future__ import annotations | ||
|
||
from pre_commit.languages import julia | ||
from testing.language_helpers import run_language | ||
|
||
|
||
def _make_hook(tmp_path, julia_code): | ||
src_dir = tmp_path.joinpath('src') | ||
src_dir.mkdir() | ||
src_dir.joinpath('main.jl').write_text(julia_code) | ||
tmp_path.joinpath('Project.toml').write_text( | ||
'[deps]\n' | ||
'Example = "7876af07-990d-54b4-ab0e-23690620f79a"\n', | ||
) | ||
|
||
|
||
def test_julia_hook(tmp_path): | ||
code = """ | ||
using Example | ||
function main() | ||
println("Hello, world!") | ||
end | ||
main() | ||
""" | ||
_make_hook(tmp_path, code) | ||
expected = (0, b'Hello, world!\n') | ||
assert run_language(tmp_path, julia, 'src/main.jl') == expected | ||
|
||
|
||
def test_julia_hook_args(tmp_path): | ||
code = """ | ||
function main(argv) | ||
foreach(println, argv) | ||
end | ||
main(ARGS) | ||
""" | ||
_make_hook(tmp_path, code) | ||
expected = (0, b'--arg1\n--arg2\n') | ||
assert run_language( | ||
tmp_path, julia, 'src/main.jl --arg1 --arg2', | ||
) == expected | ||
|
||
|
||
def test_julia_hook_additional_deps(tmp_path): | ||
code = """ | ||
using TOML | ||
function main() | ||
project_file = Base.active_project() | ||
dict = TOML.parsefile(project_file) | ||
for (k, v) in dict["deps"] | ||
println(k, " = ", v) | ||
end | ||
end | ||
main() | ||
""" | ||
_make_hook(tmp_path, code) | ||
deps = ('TOML=fa267f1f-6049-4f14-aa54-33bafae1ed76',) | ||
ret, out = run_language(tmp_path, julia, 'src/main.jl', deps=deps) | ||
assert ret == 0 | ||
assert b'Example = 7876af07-990d-54b4-ab0e-23690620f79a' in out | ||
assert b'TOML = fa267f1f-6049-4f14-aa54-33bafae1ed76' in out |