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 85783bd
Showing
3 changed files
with
231 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,132 @@ | ||
from __future__ import annotations | ||
|
||
import contextlib | ||
import os | ||
import shutil | ||
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.envcontext import UNSET | ||
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), unless | ||
# it is a local script | ||
# 3) prepend `julia` as the interpreter | ||
|
||
cmd = lang_base.hook_cmd(entry, args) | ||
script = cmd[0] if is_local else prefix.path(cmd[0]) | ||
cmd = ('julia', script, *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 ( | ||
('JULIA_LOAD_PATH', target_dir), | ||
# May be set, remove it to not interfer with LOAD_PATH | ||
('JULIA_PROJECT', UNSET), | ||
) | ||
|
||
|
||
@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 in_env(prefix, version): | ||
# TODO: Support language_version with juliaup similar to rust via | ||
# rustup | ||
# if version != 'system': | ||
# ... | ||
|
||
# Copy Project.toml to hook env if it exist | ||
os.makedirs(envdir, exist_ok=True) | ||
project_names = ('JuliaProject.toml', 'Project.toml') | ||
project_found = False | ||
for project_name in project_names: | ||
project_file = prefix.path(project_name) | ||
if not os.path.isfile(project_file): | ||
continue | ||
shutil.copy(project_file, envdir) | ||
project_found = True | ||
break | ||
|
||
# If no project file was found we create an empty one so that the | ||
# package manager doesn't error | ||
if not project_found: | ||
open(os.path.join(envdir, 'Project.toml'), 'a').close() | ||
|
||
# Copy Manifest.toml to hook env if it exists | ||
manifest_names = ('JuliaManifest.toml', 'Manifest.toml') | ||
for manifest_name in manifest_names: | ||
manifest_file = prefix.path(manifest_name) | ||
if not os.path.isfile(manifest_file): | ||
continue | ||
shutil.copy(manifest_file, envdir) | ||
break | ||
|
||
# Julia code to instantiate the hook environment | ||
julia_code = """ | ||
@assert length(ARGS) > 0 | ||
hook_env = ARGS[1] | ||
deps = join(ARGS[2: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,97 @@ | ||
from __future__ import annotations | ||
|
||
from pre_commit.languages import julia | ||
from testing.language_helpers import run_language | ||
from testing.util import cwd | ||
|
||
|
||
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_manifest(tmp_path): | ||
code = """ | ||
using Example | ||
println(pkgversion(Example)) | ||
""" | ||
_make_hook(tmp_path, code) | ||
|
||
tmp_path.joinpath('Manifest.toml').write_text( | ||
'manifest_format = "2.0"\n\n' | ||
'[[deps.Example]]\n' | ||
'git-tree-sha1 = "11820aa9c229fd3833d4bd69e5e75ef4e7273bf1"\n' | ||
'uuid = "7876af07-990d-54b4-ab0e-23690620f79a"\n' | ||
'version = "0.5.4"\n', | ||
) | ||
expected = (0, b'0.5.4\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 | ||
|
||
|
||
def test_julia_repo_local(tmp_path): | ||
env_dir = tmp_path.joinpath('envdir') | ||
env_dir.mkdir() | ||
local_dir = tmp_path.joinpath('local') | ||
local_dir.mkdir() | ||
local_dir.joinpath('local.jl').write_text( | ||
'using TOML; foreach(println, ARGS)', | ||
) | ||
with cwd(local_dir): | ||
deps = ('TOML=fa267f1f-6049-4f14-aa54-33bafae1ed76',) | ||
expected = (0, b'--local-arg1\n--local-arg2\n') | ||
assert run_language( | ||
env_dir, julia, 'local.jl --local-arg1 --local-arg2', | ||
deps=deps, is_local=True, | ||
) == expected |