mirror of
https://github.com/pre-commit/pre-commit.git
synced 2026-02-17 00:04:42 +04:00
Add support for julia hooks
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: https://github.com/pre-commit/pre-commit.com/pull/998 Fixes #2689.
This commit is contained in:
parent
9da45a686a
commit
85783bdc0b
3 changed files with 231 additions and 0 deletions
|
|
@ -10,6 +10,7 @@ from pre_commit.languages import dotnet
|
|||
from pre_commit.languages import fail
|
||||
from pre_commit.languages import golang
|
||||
from pre_commit.languages import haskell
|
||||
from pre_commit.languages import julia
|
||||
from pre_commit.languages import lua
|
||||
from pre_commit.languages import node
|
||||
from pre_commit.languages import perl
|
||||
|
|
@ -33,6 +34,7 @@ languages: dict[str, Language] = {
|
|||
'fail': fail,
|
||||
'golang': golang,
|
||||
'haskell': haskell,
|
||||
'julia': julia,
|
||||
'lua': lua,
|
||||
'node': node,
|
||||
'perl': perl,
|
||||
|
|
|
|||
132
pre_commit/languages/julia.py
Normal file
132
pre_commit/languages/julia.py
Normal file
|
|
@ -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,
|
||||
)
|
||||
97
tests/languages/julia_test.py
Normal file
97
tests/languages/julia_test.py
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue