Skip to content

Commit 74233a1

Browse files
authored
Merge pull request pre-commit#3348 from fredrikekre/fe/julia
Add support for julia hooks
2 parents 9da45a6 + 85783bd commit 74233a1

File tree

3 files changed

+231
-0
lines changed

3 files changed

+231
-0
lines changed

pre_commit/all_languages.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from pre_commit.languages import fail
1111
from pre_commit.languages import golang
1212
from pre_commit.languages import haskell
13+
from pre_commit.languages import julia
1314
from pre_commit.languages import lua
1415
from pre_commit.languages import node
1516
from pre_commit.languages import perl
@@ -33,6 +34,7 @@
3334
'fail': fail,
3435
'golang': golang,
3536
'haskell': haskell,
37+
'julia': julia,
3638
'lua': lua,
3739
'node': node,
3840
'perl': perl,

pre_commit/languages/julia.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
from __future__ import annotations
2+
3+
import contextlib
4+
import os
5+
import shutil
6+
from collections.abc import Generator
7+
from collections.abc import Sequence
8+
9+
from pre_commit import lang_base
10+
from pre_commit.envcontext import envcontext
11+
from pre_commit.envcontext import PatchesT
12+
from pre_commit.envcontext import UNSET
13+
from pre_commit.prefix import Prefix
14+
from pre_commit.util import cmd_output_b
15+
16+
ENVIRONMENT_DIR = 'juliaenv'
17+
health_check = lang_base.basic_health_check
18+
get_default_version = lang_base.basic_get_default_version
19+
20+
21+
def run_hook(
22+
prefix: Prefix,
23+
entry: str,
24+
args: Sequence[str],
25+
file_args: Sequence[str],
26+
*,
27+
is_local: bool,
28+
require_serial: bool,
29+
color: bool,
30+
) -> tuple[int, bytes]:
31+
# `entry` is a (hook-repo relative) file followed by (optional) args, e.g.
32+
# `bin/id.jl` or `bin/hook.jl --arg1 --arg2` so we
33+
# 1) shell parse it and join with args with hook_cmd
34+
# 2) prepend the hooks prefix path to the first argument (the file), unless
35+
# it is a local script
36+
# 3) prepend `julia` as the interpreter
37+
38+
cmd = lang_base.hook_cmd(entry, args)
39+
script = cmd[0] if is_local else prefix.path(cmd[0])
40+
cmd = ('julia', script, *cmd[1:])
41+
return lang_base.run_xargs(
42+
cmd,
43+
file_args,
44+
require_serial=require_serial,
45+
color=color,
46+
)
47+
48+
49+
def get_env_patch(target_dir: str, version: str) -> PatchesT:
50+
return (
51+
('JULIA_LOAD_PATH', target_dir),
52+
# May be set, remove it to not interfer with LOAD_PATH
53+
('JULIA_PROJECT', UNSET),
54+
)
55+
56+
57+
@contextlib.contextmanager
58+
def in_env(prefix: Prefix, version: str) -> Generator[None]:
59+
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
60+
with envcontext(get_env_patch(envdir, version)):
61+
yield
62+
63+
64+
def install_environment(
65+
prefix: Prefix,
66+
version: str,
67+
additional_dependencies: Sequence[str],
68+
) -> None:
69+
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
70+
with in_env(prefix, version):
71+
# TODO: Support language_version with juliaup similar to rust via
72+
# rustup
73+
# if version != 'system':
74+
# ...
75+
76+
# Copy Project.toml to hook env if it exist
77+
os.makedirs(envdir, exist_ok=True)
78+
project_names = ('JuliaProject.toml', 'Project.toml')
79+
project_found = False
80+
for project_name in project_names:
81+
project_file = prefix.path(project_name)
82+
if not os.path.isfile(project_file):
83+
continue
84+
shutil.copy(project_file, envdir)
85+
project_found = True
86+
break
87+
88+
# If no project file was found we create an empty one so that the
89+
# package manager doesn't error
90+
if not project_found:
91+
open(os.path.join(envdir, 'Project.toml'), 'a').close()
92+
93+
# Copy Manifest.toml to hook env if it exists
94+
manifest_names = ('JuliaManifest.toml', 'Manifest.toml')
95+
for manifest_name in manifest_names:
96+
manifest_file = prefix.path(manifest_name)
97+
if not os.path.isfile(manifest_file):
98+
continue
99+
shutil.copy(manifest_file, envdir)
100+
break
101+
102+
# Julia code to instantiate the hook environment
103+
julia_code = """
104+
@assert length(ARGS) > 0
105+
hook_env = ARGS[1]
106+
deps = join(ARGS[2:end], " ")
107+
108+
# We prepend @stdlib here so that we can load the package manager even
109+
# though `get_env_patch` limits `JULIA_LOAD_PATH` to just the hook env.
110+
pushfirst!(LOAD_PATH, "@stdlib")
111+
using Pkg
112+
popfirst!(LOAD_PATH)
113+
114+
# Instantiate the environment shipped with the hook repo. If we have
115+
# additional dependencies we disable precompilation in this step to
116+
# avoid double work.
117+
precompile = isempty(deps) ? "1" : "0"
118+
withenv("JULIA_PKG_PRECOMPILE_AUTO" => precompile) do
119+
Pkg.instantiate()
120+
end
121+
122+
# Add additional dependencies (with precompilation)
123+
if !isempty(deps)
124+
withenv("JULIA_PKG_PRECOMPILE_AUTO" => "1") do
125+
Pkg.REPLMode.pkgstr("add " * deps)
126+
end
127+
end
128+
"""
129+
cmd_output_b(
130+
'julia', '-e', julia_code, '--', envdir, *additional_dependencies,
131+
cwd=prefix.prefix_dir,
132+
)

tests/languages/julia_test.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
from __future__ import annotations
2+
3+
from pre_commit.languages import julia
4+
from testing.language_helpers import run_language
5+
from testing.util import cwd
6+
7+
8+
def _make_hook(tmp_path, julia_code):
9+
src_dir = tmp_path.joinpath('src')
10+
src_dir.mkdir()
11+
src_dir.joinpath('main.jl').write_text(julia_code)
12+
tmp_path.joinpath('Project.toml').write_text(
13+
'[deps]\n'
14+
'Example = "7876af07-990d-54b4-ab0e-23690620f79a"\n',
15+
)
16+
17+
18+
def test_julia_hook(tmp_path):
19+
code = """
20+
using Example
21+
function main()
22+
println("Hello, world!")
23+
end
24+
main()
25+
"""
26+
_make_hook(tmp_path, code)
27+
expected = (0, b'Hello, world!\n')
28+
assert run_language(tmp_path, julia, 'src/main.jl') == expected
29+
30+
31+
def test_julia_hook_manifest(tmp_path):
32+
code = """
33+
using Example
34+
println(pkgversion(Example))
35+
"""
36+
_make_hook(tmp_path, code)
37+
38+
tmp_path.joinpath('Manifest.toml').write_text(
39+
'manifest_format = "2.0"\n\n'
40+
'[[deps.Example]]\n'
41+
'git-tree-sha1 = "11820aa9c229fd3833d4bd69e5e75ef4e7273bf1"\n'
42+
'uuid = "7876af07-990d-54b4-ab0e-23690620f79a"\n'
43+
'version = "0.5.4"\n',
44+
)
45+
expected = (0, b'0.5.4\n')
46+
assert run_language(tmp_path, julia, 'src/main.jl') == expected
47+
48+
49+
def test_julia_hook_args(tmp_path):
50+
code = """
51+
function main(argv)
52+
foreach(println, argv)
53+
end
54+
main(ARGS)
55+
"""
56+
_make_hook(tmp_path, code)
57+
expected = (0, b'--arg1\n--arg2\n')
58+
assert run_language(
59+
tmp_path, julia, 'src/main.jl --arg1 --arg2',
60+
) == expected
61+
62+
63+
def test_julia_hook_additional_deps(tmp_path):
64+
code = """
65+
using TOML
66+
function main()
67+
project_file = Base.active_project()
68+
dict = TOML.parsefile(project_file)
69+
for (k, v) in dict["deps"]
70+
println(k, " = ", v)
71+
end
72+
end
73+
main()
74+
"""
75+
_make_hook(tmp_path, code)
76+
deps = ('TOML=fa267f1f-6049-4f14-aa54-33bafae1ed76',)
77+
ret, out = run_language(tmp_path, julia, 'src/main.jl', deps=deps)
78+
assert ret == 0
79+
assert b'Example = 7876af07-990d-54b4-ab0e-23690620f79a' in out
80+
assert b'TOML = fa267f1f-6049-4f14-aa54-33bafae1ed76' in out
81+
82+
83+
def test_julia_repo_local(tmp_path):
84+
env_dir = tmp_path.joinpath('envdir')
85+
env_dir.mkdir()
86+
local_dir = tmp_path.joinpath('local')
87+
local_dir.mkdir()
88+
local_dir.joinpath('local.jl').write_text(
89+
'using TOML; foreach(println, ARGS)',
90+
)
91+
with cwd(local_dir):
92+
deps = ('TOML=fa267f1f-6049-4f14-aa54-33bafae1ed76',)
93+
expected = (0, b'--local-arg1\n--local-arg2\n')
94+
assert run_language(
95+
env_dir, julia, 'local.jl --local-arg1 --local-arg2',
96+
deps=deps, is_local=True,
97+
) == expected

0 commit comments

Comments
 (0)