diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index cfd42ce2..5f77d2c0 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -21,6 +21,7 @@ from pre_commit.languages import python from pre_commit.languages import r from pre_commit.languages import ruby from pre_commit.languages import rust +from pre_commit.languages import sbt from pre_commit.languages import script from pre_commit.languages import swift from pre_commit.languages import system @@ -60,6 +61,7 @@ languages = { 'r': Language(name='r', ENVIRONMENT_DIR=r.ENVIRONMENT_DIR, get_default_version=r.get_default_version, health_check=r.health_check, install_environment=r.install_environment, run_hook=r.run_hook), # noqa: E501 'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, health_check=ruby.health_check, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501 'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, health_check=rust.health_check, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501 + 'sbt': Language(name='sbt', ENVIRONMENT_DIR=sbt.ENVIRONMENT_DIR, get_default_version=sbt.get_default_version, health_check=sbt.health_check, install_environment=sbt.install_environment, run_hook=sbt.run_hook), # noqa: E501 'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, health_check=script.health_check, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501 'swift': Language(name='swift', ENVIRONMENT_DIR=swift.ENVIRONMENT_DIR, get_default_version=swift.get_default_version, health_check=swift.health_check, install_environment=swift.install_environment, run_hook=swift.run_hook), # noqa: E501 'system': Language(name='system', ENVIRONMENT_DIR=system.ENVIRONMENT_DIR, get_default_version=system.get_default_version, health_check=system.health_check, install_environment=system.install_environment, run_hook=system.run_hook), # noqa: E501 diff --git a/pre_commit/languages/sbt.py b/pre_commit/languages/sbt.py new file mode 100644 index 00000000..bf8c55a2 --- /dev/null +++ b/pre_commit/languages/sbt.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from typing import Sequence + +from pre_commit.hook import Hook +from pre_commit.languages import helpers + +ENVIRONMENT_DIR = None +install_environment = helpers.no_install +health_check = helpers.basic_health_check +get_default_version = helpers.basic_get_default_version + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> tuple[int, bytes]: + # TODO: Improve impl to connect to run commands via SBT server + return run_sbt_hook_via_commandline(hook, file_args, color) + + +def run_sbt_hook_via_commandline( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> tuple[int, bytes]: + """ + Run an SBT hook, via the commandline. The command to be run is: + sbt ${entry} ${args} ${files} + The entry and args will not be quoted (so should be wrapped in quotes as + appropriate by the hook author),however files will be quoted, so any + filenames with spaces will be interpreted as a single argument by SBT + """ + entry_part = hook.entry + args_part = ' '.join(hook.args) + files_part = ' '.join(_quote(file) for file in file_args) + sbt_command = f'{entry_part} {args_part} {files_part}' + shell_cmd = ('sbt', sbt_command) + return helpers.run_xargs(hook, shell_cmd, [], color=color) + + +def _quote(s: str) -> str: + return f"\"{s}\"" diff --git a/testing/gen-languages-all b/testing/gen-languages-all index 05f89295..e3c10f2a 100755 --- a/testing/gen-languages-all +++ b/testing/gen-languages-all @@ -6,7 +6,7 @@ import sys LANGUAGES = ( 'conda', 'coursier', 'dart', 'docker', 'docker_image', 'dotnet', 'fail', 'golang', 'lua', 'node', 'perl', 'pygrep', 'python', 'r', 'ruby', 'rust', - 'script', 'swift', 'system', + 'sbt', 'script', 'swift', 'system', ) FIELDS = ( 'ENVIRONMENT_DIR', 'get_default_version', 'health_check', diff --git a/testing/resources/sbt_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/sbt_hooks_repo/.pre-commit-hooks.yaml new file mode 100644 index 00000000..cab90bc6 --- /dev/null +++ b/testing/resources/sbt_hooks_repo/.pre-commit-hooks.yaml @@ -0,0 +1,6 @@ +- id: sbt-create-files + name: touch + entry: touch + args: ["file1.txt", "\"file2 with space.txt\""] + description: "creates files provided by `args` or `files`" + language: sbt diff --git a/testing/resources/sbt_repo_with_touch_command/build.sbt b/testing/resources/sbt_repo_with_touch_command/build.sbt new file mode 100644 index 00000000..f1878d1a --- /dev/null +++ b/testing/resources/sbt_repo_with_touch_command/build.sbt @@ -0,0 +1,6 @@ +import TouchCommand._ + +lazy val root = (project in file(".")) + .settings( + commands ++= Seq(touchCommand) + ) diff --git a/testing/resources/sbt_repo_with_touch_command/project/TouchCommand.scala b/testing/resources/sbt_repo_with_touch_command/project/TouchCommand.scala new file mode 100644 index 00000000..e08d52e3 --- /dev/null +++ b/testing/resources/sbt_repo_with_touch_command/project/TouchCommand.scala @@ -0,0 +1,13 @@ +import sbt.Command + +import java.nio.file.{Files, Paths} + +object TouchCommand { + def touchCommand = Command.args("touch", "args") { (state, args) => + args.map(Paths.get(_).toAbsolutePath).foreach { path => + println(f"Creating file: $path") + Files.createFile(path) + } + state + } +} diff --git a/testing/util.py b/testing/util.py index e807f048..bf4e5980 100644 --- a/testing/util.py +++ b/testing/util.py @@ -54,6 +54,10 @@ skipif_cant_run_lua = pytest.mark.skipif( os.name == 'nt', reason="lua isn't installed or can't be found", ) +skipif_cant_run_sbt = pytest.mark.skipif( + parse_shebang.find_executable('sbt') is None, + reason="SBT isn't installed or can't be found", +) skipif_cant_run_swift = pytest.mark.skipif( parse_shebang.find_executable('swift') is None, reason="swift isn't installed or can't be found", diff --git a/tests/conftest.py b/tests/conftest.py index 30761715..d3813cc5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,6 +4,7 @@ import functools import io import logging import os.path +from pathlib import Path from unittest import mock import pytest @@ -16,6 +17,7 @@ from pre_commit.util import cmd_output from pre_commit.util import make_executable from testing.fixtures import git_dir from testing.fixtures import make_consuming_repo +from testing.fixtures import make_repo from testing.fixtures import write_config from testing.util import cwd from testing.util import git_commit @@ -250,3 +252,9 @@ def set_git_templatedir(tmpdir_factory): tdir = str(tmpdir_factory.mktemp('git_template_dir')) with envcontext((('GIT_TEMPLATE_DIR', tdir),)): yield + + +@pytest.fixture +def sbt_project_with_touch_command(tempdir_factory): + project_repo = make_repo(tempdir_factory, 'sbt_repo_with_touch_command') + return Path(project_repo) diff --git a/tests/languages/sbt_test.py b/tests/languages/sbt_test.py new file mode 100644 index 00000000..790d388a --- /dev/null +++ b/tests/languages/sbt_test.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +from itertools import product +from pathlib import Path +from typing import Any + +import pytest + +from pre_commit.hook import Hook +from pre_commit.languages import sbt +from testing.util import cwd +from testing.util import skipif_cant_run_sbt + + +@skipif_cant_run_sbt +@pytest.mark.parametrize( + ['args', 'files'], + product( + [ + [], ['argfile1.txt'], ['argfile1.txt', 'argfile2.txt'], + ['\"arg file1.txt\"'], ['\"arg file1.txt\"', '\"arg file2.txt\"'], + ], + [ + [], ['filesfile1.txt'], ['filesfile1.txt', 'filesfile2.txt'], + ['files file1.txt'], ['files file1.txt', 'files file2.txt'], + ], + ), +) +def test_sbt_hook( + sbt_project_with_touch_command: Path, + args: list[str], + files: list[str], +) -> None: + # arrange + project_root = sbt_project_with_touch_command + hook = _create_hook( + language='sbt', + entry='touch', + args=args, + ) + + # act + with cwd(project_root): + ret, out = sbt.run_hook(hook, files, False) + + # assert + output = out.decode('UTF-8') + assert ret == 0 + for file in args + files: + unquoted_file = _unquote(file) + expected_file = project_root.joinpath(unquoted_file).absolute() + assert expected_file.exists() + assert f'Creating file: {expected_file}' in output + + +def _unquote(s: str) -> str: + return s.strip("\"") + + +def _create_hook(**kwargs: Any) -> Hook: + default_values = {field: None for field in Hook._fields} + actual_values = {**default_values, **kwargs} + return Hook(**actual_values) # type: ignore diff --git a/tests/repository_test.py b/tests/repository_test.py index 8705d886..9dbbb687 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -35,6 +35,7 @@ from testing.util import get_resource_path from testing.util import skipif_cant_run_coursier from testing.util import skipif_cant_run_docker from testing.util import skipif_cant_run_lua +from testing.util import skipif_cant_run_sbt from testing.util import skipif_cant_run_swift from testing.util import xfailif_windows @@ -1150,3 +1151,40 @@ def test_local_lua_additional_dependencies(store): ret, out = _hook_run(hook, (), color=False) assert b'Luacheck' in out assert ret == 0 + + +@skipif_cant_run_sbt +def test_sbt_hook( + sbt_project_with_touch_command, + tempdir_factory, + store, +): + # arrange + project_root = sbt_project_with_touch_command + hooks_repo = make_repo(tempdir_factory, 'sbt_hooks_repo') + config = make_config_from_repo(hooks_repo) + hook = _get_hook(config, store, 'sbt-create-files') + + # act + with cwd(project_root): + ret, out = _hook_run( + hook, + ['file3.txt', 'file4 with space.txt'], + color=False, + ) + + # assert + output = out.decode('UTF-8') + assert ret == 0 + file1 = project_root.joinpath('file1.txt').absolute() + assert file1.exists() + assert f'Creating file: {file1}' in output + file2 = project_root.joinpath('file2 with space.txt').absolute() + assert file2.exists() + assert f'Creating file: {file2}' in output + file3 = project_root.joinpath('file3.txt').absolute() + assert file3.exists() + assert f'Creating file: {file3}' in output + file4 = project_root.joinpath('file4 with space.txt').absolute() + assert file4.exists() + assert f'Creating file: {file4}' in output