-
-
Notifications
You must be signed in to change notification settings - Fork 940
Add support for julia hooks #3348
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe I just don't know enough about julia but from a quick search it seems like TOML is a stdlib library? does it still need to be installed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is a standard library, and it can be loaded at top level (e.g. in the Julia REPL) by default, since the
@stdlibentry is included in the defaultLOAD_PATH. However, for Julia packages it is required (so people are used to explicitly adding also stdlibs), and for full reproducibility it is recommended. There is also ongoing work to separate stdlibs to make them less tied to the Julia version, so then they would behave more like normal packages that can be upgraded separately etc.We can of course make the choice to include
@stdlibin the load path for julia hooks, but I would argue that it is better to be explicit with the dependencies in this case.