summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/languages.yaml2
-rw-r--r--.pre-commit-config.yaml8
-rw-r--r--CHANGELOG.md18
-rw-r--r--pre_commit/all_languages.py2
-rw-r--r--pre_commit/languages/golang.py2
-rw-r--r--pre_commit/languages/julia.py132
-rw-r--r--pre_commit/languages/r.py48
-rw-r--r--setup.cfg2
-rwxr-xr-xtesting/make-archives3
-rw-r--r--tests/languages/docker_image_test.py8
-rw-r--r--tests/languages/dotnet_test.py2
-rw-r--r--tests/languages/golang_test.py69
-rw-r--r--tests/languages/julia_test.py97
-rw-r--r--tests/languages/r_test.py2
14 files changed, 374 insertions, 21 deletions
diff --git a/.github/workflows/languages.yaml b/.github/workflows/languages.yaml
index 7d50535..61293a0 100644
--- a/.github/workflows/languages.yaml
+++ b/.github/workflows/languages.yaml
@@ -65,6 +65,8 @@ jobs:
if: matrix.os == 'windows-latest' && matrix.language == 'perl'
- uses: haskell/actions/setup@v2
if: matrix.language == 'haskell'
+ - uses: r-lib/actions/setup-r@v2
+ if: matrix.os == 'ubuntu-latest' && matrix.language == 'r'
- name: install deps
run: python -mpip install -e . -r requirements-dev.txt
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7bd2611..4a23da2 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -10,11 +10,11 @@ repos:
- id: name-tests-test
- id: requirements-txt-fixer
- repo: https://github.com/asottile/setup-cfg-fmt
- rev: v2.5.0
+ rev: v2.7.0
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/asottile/reorder-python-imports
- rev: v3.13.0
+ rev: v3.14.0
hooks:
- id: reorder-python-imports
exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/)
@@ -24,7 +24,7 @@ repos:
hooks:
- id: add-trailing-comma
- repo: https://github.com/asottile/pyupgrade
- rev: v3.17.0
+ rev: v3.19.1
hooks:
- id: pyupgrade
args: [--py39-plus]
@@ -37,7 +37,7 @@ repos:
hooks:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.11.2
+ rev: v1.14.1
hooks:
- id: mypy
additional_dependencies: [types-pyyaml]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a9b4c8c..408afe6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,21 @@
+4.1.0 - 2025-01-20
+==================
+
+### Features
+- Add `language: julia`.
+ - #3348 PR by @fredrikekre.
+ - #2689 issue @jmuchovej.
+
+### Fixes
+- Disable automatic toolchain switching for `language: golang`.
+ - #3304 PR by @AleksaC.
+ - #3300 issue by @AleksaC.
+ - #3149 issue by @nijel.
+- Fix `language: r` installation when initiated by RStudio.
+ - #3389 PR by @lorenzwalthert.
+ - #3385 issue by @lorenzwalthert.
+
+
4.0.1 - 2024-10-08
==================
diff --git a/pre_commit/all_languages.py b/pre_commit/all_languages.py
index f2d11bb..ba569c3 100644
--- a/pre_commit/all_languages.py
+++ b/pre_commit/all_languages.py
@@ -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,
diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py
index 6090879..678c04b 100644
--- a/pre_commit/languages/golang.py
+++ b/pre_commit/languages/golang.py
@@ -75,6 +75,7 @@ def get_env_patch(venv: str, version: str) -> PatchesT:
return (
('GOROOT', os.path.join(venv, '.go')),
+ ('GOTOOLCHAIN', 'local'),
(
'PATH', (
os.path.join(venv, 'bin'), os.pathsep,
@@ -145,6 +146,7 @@ def install_environment(
env = no_git_env(dict(os.environ, GOPATH=gopath))
env.pop('GOBIN', None)
if version != 'system':
+ env['GOTOOLCHAIN'] = 'local'
env['GOROOT'] = os.path.join(env_dir, '.go')
env['PATH'] = os.pathsep.join((
os.path.join(env_dir, '.go', 'bin'), os.environ['PATH'],
diff --git a/pre_commit/languages/julia.py b/pre_commit/languages/julia.py
new file mode 100644
index 0000000..df91c06
--- /dev/null
+++ b/pre_commit/languages/julia.py
@@ -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,
+ )
diff --git a/pre_commit/languages/r.py b/pre_commit/languages/r.py
index c75a308..f70d2fd 100644
--- a/pre_commit/languages/r.py
+++ b/pre_commit/languages/r.py
@@ -15,27 +15,50 @@ 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
-from pre_commit.util import cmd_output_b
from pre_commit.util import win_exe
ENVIRONMENT_DIR = 'renv'
-RSCRIPT_OPTS = ('--no-save', '--no-restore', '--no-site-file', '--no-environ')
get_default_version = lang_base.basic_get_default_version
+_RENV_ACTIVATED_OPTS = (
+ '--no-save', '--no-restore', '--no-site-file', '--no-environ',
+)
-def _execute_vanilla_r_code_as_script(
+
+def _execute_r(
code: str, *,
prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str,
+ cli_opts: Sequence[str],
) -> str:
with in_env(prefix, version), _r_code_in_tempfile(code) as f:
_, out, _ = cmd_output(
- _rscript_exec(), *RSCRIPT_OPTS, f, *args, cwd=cwd,
+ _rscript_exec(), *cli_opts, f, *args, cwd=cwd,
)
return out.rstrip('\n')
+def _execute_r_in_renv(
+ code: str, *,
+ prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str,
+) -> str:
+ return _execute_r(
+ code=code, prefix=prefix, version=version, args=args, cwd=cwd,
+ cli_opts=_RENV_ACTIVATED_OPTS,
+ )
+
+
+def _execute_vanilla_r(
+ code: str, *,
+ prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str,
+) -> str:
+ return _execute_r(
+ code=code, prefix=prefix, version=version, args=args, cwd=cwd,
+ cli_opts=('--vanilla',),
+ )
+
+
def _read_installed_version(envdir: str, prefix: Prefix, version: str) -> str:
- return _execute_vanilla_r_code_as_script(
+ return _execute_r_in_renv(
'cat(renv::settings$r.version())',
prefix=prefix, version=version,
cwd=envdir,
@@ -43,7 +66,7 @@ def _read_installed_version(envdir: str, prefix: Prefix, version: str) -> str:
def _read_executable_version(envdir: str, prefix: Prefix, version: str) -> str:
- return _execute_vanilla_r_code_as_script(
+ return _execute_r_in_renv(
'cat(as.character(getRversion()))',
prefix=prefix, version=version,
cwd=envdir,
@@ -53,7 +76,7 @@ def _read_executable_version(envdir: str, prefix: Prefix, version: str) -> str:
def _write_current_r_version(
envdir: str, prefix: Prefix, version: str,
) -> None:
- _execute_vanilla_r_code_as_script(
+ _execute_r_in_renv(
'renv::settings$r.version(as.character(getRversion()))',
prefix=prefix, version=version,
cwd=envdir,
@@ -161,7 +184,7 @@ def _cmd_from_hook(
_entry_validate(cmd)
cmd_part = _prefix_if_file_entry(cmd, prefix, is_local=is_local)
- return (cmd[0], *RSCRIPT_OPTS, *cmd_part, *args)
+ return (cmd[0], *_RENV_ACTIVATED_OPTS, *cmd_part, *args)
def install_environment(
@@ -204,14 +227,15 @@ def install_environment(
renv::install(prefix_dir)
}}
"""
-
- with _r_code_in_tempfile(r_code_inst_environment) as f:
- cmd_output_b(_rscript_exec(), '--vanilla', f, cwd=env_dir)
+ _execute_vanilla_r(
+ r_code_inst_environment,
+ prefix=prefix, version=version, cwd=env_dir,
+ )
_write_current_r_version(envdir=env_dir, prefix=prefix, version=version)
if additional_dependencies:
r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))'
- _execute_vanilla_r_code_as_script(
+ _execute_r_in_renv(
code=r_code_inst_add, prefix=prefix, version=version,
args=additional_dependencies,
cwd=env_dir,
diff --git a/setup.cfg b/setup.cfg
index 6936a1f..60d9764 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
[metadata]
name = pre_commit
-version = 4.0.1
+version = 4.1.0
description = A framework for managing and maintaining multi-language pre-commit hooks.
long_description = file: README.md
long_description_content_type = text/markdown
diff --git a/testing/make-archives b/testing/make-archives
index 251be4a..eb3f3af 100755
--- a/testing/make-archives
+++ b/testing/make-archives
@@ -57,8 +57,7 @@ def make_archive(name: str, repo: str, ref: str, destdir: str) -> str:
arcs.sort()
with gzip.GzipFile(output_path, 'wb', mtime=0) as gzipf:
- # https://github.com/python/typeshed/issues/5491
- with tarfile.open(fileobj=gzipf, mode='w') as tf: # type: ignore
+ with tarfile.open(fileobj=gzipf, mode='w') as tf:
for arcname, abspath in arcs:
tf.add(
abspath,
diff --git a/tests/languages/docker_image_test.py b/tests/languages/docker_image_test.py
index 4e3a878..4f72060 100644
--- a/tests/languages/docker_image_test.py
+++ b/tests/languages/docker_image_test.py
@@ -1,10 +1,18 @@
from __future__ import annotations
+import pytest
+
from pre_commit.languages import docker_image
+from pre_commit.util import cmd_output_b
from testing.language_helpers import run_language
from testing.util import xfailif_windows
+@pytest.fixture(autouse=True, scope='module')
+def _ensure_image_available():
+ cmd_output_b('docker', 'run', '--rm', 'ubuntu:22.04', 'echo')
+
+
@xfailif_windows # pragma: win32 no cover
def test_docker_image_hook_via_entrypoint(tmp_path):
ret = run_language(
diff --git a/tests/languages/dotnet_test.py b/tests/languages/dotnet_test.py
index 470c03b..ee40825 100644
--- a/tests/languages/dotnet_test.py
+++ b/tests/languages/dotnet_test.py
@@ -27,7 +27,7 @@ def _csproj(tool_name):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
- <TargetFramework>net6</TargetFramework>
+ <TargetFramework>net8</TargetFramework>
<PackAsTool>true</PackAsTool>
<ToolCommandName>{tool_name}</ToolCommandName>
<PackageOutputPath>./nupkg</PackageOutputPath>
diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py
index 02e35d7..7fb6ab1 100644
--- a/tests/languages/golang_test.py
+++ b/tests/languages/golang_test.py
@@ -11,11 +11,13 @@ from pre_commit.commands.install_uninstall import install
from pre_commit.envcontext import envcontext
from pre_commit.languages import golang
from pre_commit.store import _make_local_repo
+from pre_commit.util import CalledProcessError
from pre_commit.util import cmd_output
from testing.fixtures import add_config_to_repo
from testing.fixtures import make_config_from_repo
from testing.language_helpers import run_language
from testing.util import cmd_output_mocked_pre_commit_home
+from testing.util import cwd
from testing.util import git_commit
@@ -165,3 +167,70 @@ def test_during_commit_all(tmp_path, tempdir_factory, store, in_git_dir):
fn=cmd_output_mocked_pre_commit_home,
tempdir_factory=tempdir_factory,
)
+
+
+def test_automatic_toolchain_switching(tmp_path):
+ go_mod = '''\
+module toolchain-version-test
+
+go 1.23.1
+'''
+ main_go = '''\
+package main
+
+func main() {}
+'''
+ tmp_path.joinpath('go.mod').write_text(go_mod)
+ mod_dir = tmp_path.joinpath('toolchain-version-test')
+ mod_dir.mkdir()
+ main_file = mod_dir.joinpath('main.go')
+ main_file.write_text(main_go)
+
+ with pytest.raises(CalledProcessError) as excinfo:
+ run_language(
+ path=tmp_path,
+ language=golang,
+ version='1.22.0',
+ exe='golang-version-test',
+ )
+
+ assert 'go.mod requires go >= 1.23.1' in excinfo.value.stderr.decode()
+
+
+def test_automatic_toolchain_switching_go_fmt(tmp_path, monkeypatch):
+ go_mod_hook = '''\
+module toolchain-version-test
+
+go 1.22.0
+'''
+ go_mod = '''\
+module toolchain-version-test
+
+go 1.23.1
+'''
+ main_go = '''\
+package main
+
+func main() {}
+'''
+ hook_dir = tmp_path.joinpath('hook')
+ hook_dir.mkdir()
+ hook_dir.joinpath('go.mod').write_text(go_mod_hook)
+
+ test_dir = tmp_path.joinpath('test')
+ test_dir.mkdir()
+ test_dir.joinpath('go.mod').write_text(go_mod)
+ main_file = test_dir.joinpath('main.go')
+ main_file.write_text(main_go)
+
+ with cwd(test_dir):
+ ret, out = run_language(
+ path=hook_dir,
+ language=golang,
+ version='1.22.0',
+ exe='go fmt',
+ file_args=(str(main_file),),
+ )
+
+ assert ret == 1
+ assert 'go.mod requires go >= 1.23.1' in out.decode()
diff --git a/tests/languages/julia_test.py b/tests/languages/julia_test.py
new file mode 100644
index 0000000..4ea3c25
--- /dev/null
+++ b/tests/languages/julia_test.py
@@ -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
diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py
index 10919e4..9e73129 100644
--- a/tests/languages/r_test.py
+++ b/tests/languages/r_test.py
@@ -286,7 +286,7 @@ def test_health_check_without_version(prefix, installed_environment, version):
prefix, env_dir = installed_environment
# simulate old pre-commit install by unsetting the installed version
- r._execute_vanilla_r_code_as_script(
+ r._execute_r_in_renv(
f'renv::settings$r.version({version})',
prefix=prefix, version=C.DEFAULT, cwd=env_dir,
)