diff options
Diffstat (limited to 'pre_commit/languages')
-rw-r--r-- | pre_commit/languages/__init__.py | 0 | ||||
-rw-r--r-- | pre_commit/languages/all.py | 60 | ||||
-rw-r--r-- | pre_commit/languages/conda.py | 84 | ||||
-rw-r--r-- | pre_commit/languages/docker.py | 114 | ||||
-rw-r--r-- | pre_commit/languages/docker_image.py | 22 | ||||
-rw-r--r-- | pre_commit/languages/fail.py | 20 | ||||
-rw-r--r-- | pre_commit/languages/golang.py | 97 | ||||
-rw-r--r-- | pre_commit/languages/helpers.py | 109 | ||||
-rw-r--r-- | pre_commit/languages/node.py | 93 | ||||
-rw-r--r-- | pre_commit/languages/perl.py | 67 | ||||
-rw-r--r-- | pre_commit/languages/pygrep.py | 87 | ||||
-rw-r--r-- | pre_commit/languages/python.py | 210 | ||||
-rw-r--r-- | pre_commit/languages/python_venv.py | 46 | ||||
-rw-r--r-- | pre_commit/languages/ruby.py | 126 | ||||
-rw-r--r-- | pre_commit/languages/rust.py | 106 | ||||
-rw-r--r-- | pre_commit/languages/script.py | 19 | ||||
-rw-r--r-- | pre_commit/languages/swift.py | 64 | ||||
-rw-r--r-- | pre_commit/languages/system.py | 19 |
18 files changed, 1343 insertions, 0 deletions
diff --git a/pre_commit/languages/__init__.py b/pre_commit/languages/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pre_commit/languages/__init__.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py new file mode 100644 index 0000000..8f4ffa8 --- /dev/null +++ b/pre_commit/languages/all.py @@ -0,0 +1,60 @@ +from typing import Callable +from typing import NamedTuple +from typing import Optional +from typing import Sequence +from typing import Tuple + +from pre_commit.hook import Hook +from pre_commit.languages import conda +from pre_commit.languages import docker +from pre_commit.languages import docker_image +from pre_commit.languages import fail +from pre_commit.languages import golang +from pre_commit.languages import node +from pre_commit.languages import perl +from pre_commit.languages import pygrep +from pre_commit.languages import python +from pre_commit.languages import python_venv +from pre_commit.languages import ruby +from pre_commit.languages import rust +from pre_commit.languages import script +from pre_commit.languages import swift +from pre_commit.languages import system +from pre_commit.prefix import Prefix + + +class Language(NamedTuple): + name: str + # Use `None` for no installation / environment + ENVIRONMENT_DIR: Optional[str] + # return a value to replace `'default` for `language_version` + get_default_version: Callable[[], str] + # return whether the environment is healthy (or should be rebuilt) + healthy: Callable[[Prefix, str], bool] + # install a repository for the given language and language_version + install_environment: Callable[[Prefix, str, Sequence[str]], None] + # execute a hook and return the exit code and output + run_hook: 'Callable[[Hook, Sequence[str], bool], Tuple[int, bytes]]' + + +# TODO: back to modules + Protocol: https://github.com/python/mypy/issues/5018 +languages = { + # BEGIN GENERATED (testing/gen-languages-all) + 'conda': Language(name='conda', ENVIRONMENT_DIR=conda.ENVIRONMENT_DIR, get_default_version=conda.get_default_version, healthy=conda.healthy, install_environment=conda.install_environment, run_hook=conda.run_hook), # noqa: E501 + 'docker': Language(name='docker', ENVIRONMENT_DIR=docker.ENVIRONMENT_DIR, get_default_version=docker.get_default_version, healthy=docker.healthy, install_environment=docker.install_environment, run_hook=docker.run_hook), # noqa: E501 + 'docker_image': Language(name='docker_image', ENVIRONMENT_DIR=docker_image.ENVIRONMENT_DIR, get_default_version=docker_image.get_default_version, healthy=docker_image.healthy, install_environment=docker_image.install_environment, run_hook=docker_image.run_hook), # noqa: E501 + 'fail': Language(name='fail', ENVIRONMENT_DIR=fail.ENVIRONMENT_DIR, get_default_version=fail.get_default_version, healthy=fail.healthy, install_environment=fail.install_environment, run_hook=fail.run_hook), # noqa: E501 + 'golang': Language(name='golang', ENVIRONMENT_DIR=golang.ENVIRONMENT_DIR, get_default_version=golang.get_default_version, healthy=golang.healthy, install_environment=golang.install_environment, run_hook=golang.run_hook), # noqa: E501 + 'node': Language(name='node', ENVIRONMENT_DIR=node.ENVIRONMENT_DIR, get_default_version=node.get_default_version, healthy=node.healthy, install_environment=node.install_environment, run_hook=node.run_hook), # noqa: E501 + 'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, healthy=perl.healthy, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501 + 'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501 + 'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, healthy=python.healthy, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501 + 'python_venv': Language(name='python_venv', ENVIRONMENT_DIR=python_venv.ENVIRONMENT_DIR, get_default_version=python_venv.get_default_version, healthy=python_venv.healthy, install_environment=python_venv.install_environment, run_hook=python_venv.run_hook), # noqa: E501 + 'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, healthy=ruby.healthy, 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, healthy=rust.healthy, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501 + 'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, healthy=script.healthy, 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, healthy=swift.healthy, 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, healthy=system.healthy, install_environment=system.install_environment, run_hook=system.run_hook), # noqa: E501 + # END GENERATED +} +all_languages = sorted(languages) diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py new file mode 100644 index 0000000..071757a --- /dev/null +++ b/pre_commit/languages/conda.py @@ -0,0 +1,84 @@ +import contextlib +import os +from typing import Generator +from typing import Sequence +from typing import Tuple + +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import SubstitutionT +from pre_commit.envcontext import UNSET +from pre_commit.envcontext import Var +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.prefix import Prefix +from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output_b + +ENVIRONMENT_DIR = 'conda' +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def get_env_patch(env: str) -> PatchesT: + # On non-windows systems executable live in $CONDA_PREFIX/bin, on Windows + # they can be in $CONDA_PREFIX/bin, $CONDA_PREFIX/Library/bin, + # $CONDA_PREFIX/Scripts and $CONDA_PREFIX. Whereas the latter only + # seems to be used for python.exe. + path: SubstitutionT = (os.path.join(env, 'bin'), os.pathsep, Var('PATH')) + if os.name == 'nt': # pragma: no cover (platform specific) + path = (env, os.pathsep, *path) + path = (os.path.join(env, 'Scripts'), os.pathsep, *path) + path = (os.path.join(env, 'Library', 'bin'), os.pathsep, *path) + + return ( + ('PYTHONHOME', UNSET), + ('VIRTUAL_ENV', UNSET), + ('CONDA_PREFIX', env), + ('PATH', path), + ) + + +@contextlib.contextmanager +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: + directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version) + envdir = prefix.path(directory) + with envcontext(get_env_patch(envdir)): + yield + + +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: + helpers.assert_version_default('conda', version) + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + + env_dir = prefix.path(directory) + with clean_path_on_failure(env_dir): + cmd_output_b( + 'conda', 'env', 'create', '-p', env_dir, '--file', + 'environment.yml', cwd=prefix.prefix_dir, + ) + if additional_dependencies: + cmd_output_b( + 'conda', 'install', '-p', env_dir, *additional_dependencies, + cwd=prefix.prefix_dir, + ) + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: + # TODO: Some rare commands need to be run using `conda run` but mostly we + # can run them withot which is much quicker and produces a better + # output. + # cmd = ('conda', 'run', '-p', env_dir) + hook.cmd + with in_env(hook.prefix, hook.language_version): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/docker.py b/pre_commit/languages/docker.py new file mode 100644 index 0000000..f449584 --- /dev/null +++ b/pre_commit/languages/docker.py @@ -0,0 +1,114 @@ +import hashlib +import os +from typing import Sequence +from typing import Tuple + +import pre_commit.constants as C +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.prefix import Prefix +from pre_commit.util import CalledProcessError +from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output_b + +ENVIRONMENT_DIR = 'docker' +PRE_COMMIT_LABEL = 'PRE_COMMIT' +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def md5(s: str) -> str: # pragma: win32 no cover + return hashlib.md5(s.encode()).hexdigest() + + +def docker_tag(prefix: Prefix) -> str: # pragma: win32 no cover + md5sum = md5(os.path.basename(prefix.prefix_dir)).lower() + return f'pre-commit-{md5sum}' + + +def docker_is_running() -> bool: # pragma: win32 no cover + try: + cmd_output_b('docker', 'ps') + except CalledProcessError: + return False + else: + return True + + +def assert_docker_available() -> None: # pragma: win32 no cover + assert docker_is_running(), ( + 'Docker is either not running or not configured in this environment' + ) + + +def build_docker_image( + prefix: Prefix, + *, + pull: bool, +) -> None: # pragma: win32 no cover + cmd: Tuple[str, ...] = ( + 'docker', 'build', + '--tag', docker_tag(prefix), + '--label', PRE_COMMIT_LABEL, + ) + if pull: + cmd += ('--pull',) + # This must come last for old versions of docker. See #477 + cmd += ('.',) + helpers.run_setup_cmd(prefix, cmd) + + +def install_environment( + prefix: Prefix, version: str, additional_dependencies: Sequence[str], +) -> None: # pragma: win32 no cover + helpers.assert_version_default('docker', version) + helpers.assert_no_additional_deps('docker', additional_dependencies) + assert_docker_available() + + directory = prefix.path( + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), + ) + + # Docker doesn't really have relevant disk environment, but pre-commit + # still needs to cleanup its state files on failure + with clean_path_on_failure(directory): + build_docker_image(prefix, pull=True) + os.mkdir(directory) + + +def get_docker_user() -> str: # pragma: win32 no cover + try: + return f'{os.getuid()}:{os.getgid()}' + except AttributeError: + return '1000:1000' + + +def docker_cmd() -> Tuple[str, ...]: # pragma: win32 no cover + return ( + 'docker', 'run', + '--rm', + '-u', get_docker_user(), + # https://docs.docker.com/engine/reference/commandline/run/#mount-volumes-from-container-volumes-from + # The `Z` option tells Docker to label the content with a private + # unshared label. Only the current container can use a private volume. + '-v', f'{os.getcwd()}:/src:rw,Z', + '--workdir', '/src', + ) + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: # pragma: win32 no cover + assert_docker_available() + # Rebuild the docker image in case it has gone missing, as many people do + # automated cleanup of docker images. + build_docker_image(hook.prefix, pull=False) + + hook_cmd = hook.cmd + entry_exe, cmd_rest = hook.cmd[0], hook_cmd[1:] + + entry_tag = ('--entrypoint', entry_exe, docker_tag(hook.prefix)) + cmd = docker_cmd() + entry_tag + cmd_rest + return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/docker_image.py b/pre_commit/languages/docker_image.py new file mode 100644 index 0000000..0c51df6 --- /dev/null +++ b/pre_commit/languages/docker_image.py @@ -0,0 +1,22 @@ +from typing import Sequence +from typing import Tuple + +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.languages.docker import assert_docker_available +from pre_commit.languages.docker import docker_cmd + +ENVIRONMENT_DIR = None +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy +install_environment = helpers.no_install + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: # pragma: win32 no cover + assert_docker_available() + cmd = docker_cmd() + hook.cmd + return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/fail.py b/pre_commit/languages/fail.py new file mode 100644 index 0000000..d2b02d2 --- /dev/null +++ b/pre_commit/languages/fail.py @@ -0,0 +1,20 @@ +from typing import Sequence +from typing import Tuple + +from pre_commit.hook import Hook +from pre_commit.languages import helpers + +ENVIRONMENT_DIR = None +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy +install_environment = helpers.no_install + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: + out = f'{hook.entry}\n\n'.encode() + out += b'\n'.join(f.encode() for f in file_args) + b'\n' + return 1, out diff --git a/pre_commit/languages/golang.py b/pre_commit/languages/golang.py new file mode 100644 index 0000000..91ade1e --- /dev/null +++ b/pre_commit/languages/golang.py @@ -0,0 +1,97 @@ +import contextlib +import os.path +import sys +from typing import Generator +from typing import Sequence +from typing import Tuple + +import pre_commit.constants as C +from pre_commit import git +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import Var +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.prefix import Prefix +from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b +from pre_commit.util import rmtree + +ENVIRONMENT_DIR = 'golangenv' +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def get_env_patch(venv: str) -> PatchesT: + return ( + ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), + ) + + +@contextlib.contextmanager +def in_env(prefix: Prefix) -> Generator[None, None, None]: + envdir = prefix.path( + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), + ) + with envcontext(get_env_patch(envdir)): + yield + + +def guess_go_dir(remote_url: str) -> str: + if remote_url.endswith('.git'): + remote_url = remote_url[:-1 * len('.git')] + looks_like_url = ( + not remote_url.startswith('file://') and + ('//' in remote_url or '@' in remote_url) + ) + remote_url = remote_url.replace(':', '/') + if looks_like_url: + _, _, remote_url = remote_url.rpartition('//') + _, _, remote_url = remote_url.rpartition('@') + return remote_url + else: + return 'unknown_src_dir' + + +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: + helpers.assert_version_default('golang', version) + directory = prefix.path( + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), + ) + + with clean_path_on_failure(directory): + remote = git.get_remote_url(prefix.prefix_dir) + repo_src_dir = os.path.join(directory, 'src', guess_go_dir(remote)) + + # Clone into the goenv we'll create + helpers.run_setup_cmd(prefix, ('git', 'clone', '.', repo_src_dir)) + + if sys.platform == 'cygwin': # pragma: no cover + _, gopath, _ = cmd_output('cygpath', '-w', directory) + gopath = gopath.strip() + else: + gopath = directory + env = dict(os.environ, GOPATH=gopath) + env.pop('GOBIN', None) + cmd_output_b('go', 'get', './...', cwd=repo_src_dir, env=env) + for dependency in additional_dependencies: + cmd_output_b('go', 'get', dependency, cwd=repo_src_dir, env=env) + # Same some disk space, we don't need these after installation + rmtree(prefix.path(directory, 'src')) + pkgdir = prefix.path(directory, 'pkg') + if os.path.exists(pkgdir): # pragma: no cover (go<1.10) + rmtree(pkgdir) + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: + with in_env(hook.prefix): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py new file mode 100644 index 0000000..b5c95e5 --- /dev/null +++ b/pre_commit/languages/helpers.py @@ -0,0 +1,109 @@ +import multiprocessing +import os +import random +from typing import Any +from typing import List +from typing import Optional +from typing import overload +from typing import Sequence +from typing import Tuple +from typing import TYPE_CHECKING + +import pre_commit.constants as C +from pre_commit.hook import Hook +from pre_commit.prefix import Prefix +from pre_commit.util import cmd_output_b +from pre_commit.xargs import xargs + +if TYPE_CHECKING: + from typing import NoReturn + +FIXED_RANDOM_SEED = 1542676186 + + +def run_setup_cmd(prefix: Prefix, cmd: Tuple[str, ...]) -> None: + cmd_output_b(*cmd, cwd=prefix.prefix_dir) + + +@overload +def environment_dir(d: None, language_version: str) -> None: ... +@overload +def environment_dir(d: str, language_version: str) -> str: ... + + +def environment_dir(d: Optional[str], language_version: str) -> Optional[str]: + if d is None: + return None + else: + return f'{d}-{language_version}' + + +def assert_version_default(binary: str, version: str) -> None: + if version != C.DEFAULT: + raise AssertionError( + f'For now, pre-commit requires system-installed {binary}', + ) + + +def assert_no_additional_deps( + lang: str, + additional_deps: Sequence[str], +) -> None: + if additional_deps: + raise AssertionError( + f'For now, pre-commit does not support ' + f'additional_dependencies for {lang}', + ) + + +def basic_get_default_version() -> str: + return C.DEFAULT + + +def basic_healthy(prefix: Prefix, language_version: str) -> bool: + return True + + +def no_install( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> 'NoReturn': + raise AssertionError('This type is not installable') + + +def target_concurrency(hook: Hook) -> int: + if hook.require_serial or 'PRE_COMMIT_NO_CONCURRENCY' in os.environ: + return 1 + else: + # Travis appears to have a bunch of CPUs, but we can't use them all. + if 'TRAVIS' in os.environ: + return 2 + else: + try: + return multiprocessing.cpu_count() + except NotImplementedError: + return 1 + + +def _shuffled(seq: Sequence[str]) -> List[str]: + """Deterministically shuffle""" + fixed_random = random.Random() + fixed_random.seed(FIXED_RANDOM_SEED, version=1) + + seq = list(seq) + random.shuffle(seq, random=fixed_random.random) + return seq + + +def run_xargs( + hook: Hook, + cmd: Tuple[str, ...], + file_args: Sequence[str], + **kwargs: Any, +) -> Tuple[int, bytes]: + # Shuffle the files so that they more evenly fill out the xargs partitions, + # but do it deterministically in case a hook cares about ordering. + file_args = _shuffled(file_args) + kwargs['target_concurrency'] = target_concurrency(hook) + return xargs(cmd, file_args, **kwargs) diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py new file mode 100644 index 0000000..79ff807 --- /dev/null +++ b/pre_commit/languages/node.py @@ -0,0 +1,93 @@ +import contextlib +import os +import sys +from typing import Generator +from typing import Sequence +from typing import Tuple + +import pre_commit.constants as C +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import Var +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.languages.python import bin_dir +from pre_commit.prefix import Prefix +from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b + +ENVIRONMENT_DIR = 'node_env' +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def _envdir(prefix: Prefix, version: str) -> str: + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + return prefix.path(directory) + + +def get_env_patch(venv: str) -> PatchesT: + if sys.platform == 'cygwin': # pragma: no cover + _, win_venv, _ = cmd_output('cygpath', '-w', venv) + install_prefix = fr'{win_venv.strip()}\bin' + lib_dir = 'lib' + elif sys.platform == 'win32': # pragma: no cover + install_prefix = bin_dir(venv) + lib_dir = 'Scripts' + else: # pragma: win32 no cover + install_prefix = venv + lib_dir = 'lib' + return ( + ('NODE_VIRTUAL_ENV', venv), + ('NPM_CONFIG_PREFIX', install_prefix), + ('npm_config_prefix', install_prefix), + ('NODE_PATH', os.path.join(venv, lib_dir, 'node_modules')), + ('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))), + ) + + +@contextlib.contextmanager +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: + with envcontext(get_env_patch(_envdir(prefix, language_version))): + yield + + +def install_environment( + prefix: Prefix, version: str, additional_dependencies: Sequence[str], +) -> None: + additional_dependencies = tuple(additional_dependencies) + assert prefix.exists('package.json') + envdir = _envdir(prefix, version) + + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx?f=255&MSPPError=-2147217396#maxpath + if sys.platform == 'win32': # pragma: no cover + envdir = f'\\\\?\\{os.path.normpath(envdir)}' + with clean_path_on_failure(envdir): + cmd = [ + sys.executable, '-mnodeenv', '--prebuilt', '--clean-src', envdir, + ] + if version != C.DEFAULT: + cmd.extend(['-n', version]) + cmd_output_b(*cmd) + + with in_env(prefix, version): + # https://npm.community/t/npm-install-g-git-vs-git-clone-cd-npm-install-g/5449 + # install as if we installed from git + helpers.run_setup_cmd(prefix, ('npm', 'install')) + helpers.run_setup_cmd( + prefix, + ('npm', 'install', '-g', '.', *additional_dependencies), + ) + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: + with in_env(hook.prefix, hook.language_version): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/perl.py b/pre_commit/languages/perl.py new file mode 100644 index 0000000..bbf5504 --- /dev/null +++ b/pre_commit/languages/perl.py @@ -0,0 +1,67 @@ +import contextlib +import os +import shlex +from typing import Generator +from typing import Sequence +from typing import Tuple + +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import Var +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.prefix import Prefix +from pre_commit.util import clean_path_on_failure + +ENVIRONMENT_DIR = 'perl_env' +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def _envdir(prefix: Prefix, version: str) -> str: + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + return prefix.path(directory) + + +def get_env_patch(venv: str) -> PatchesT: + return ( + ('PATH', (os.path.join(venv, 'bin'), os.pathsep, Var('PATH'))), + ('PERL5LIB', os.path.join(venv, 'lib', 'perl5')), + ('PERL_MB_OPT', f'--install_base {shlex.quote(venv)}'), + ( + 'PERL_MM_OPT', ( + f'INSTALL_BASE={shlex.quote(venv)} ' + f'INSTALLSITEMAN1DIR=none INSTALLSITEMAN3DIR=none' + ), + ), + ) + + +@contextlib.contextmanager +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: + with envcontext(get_env_patch(_envdir(prefix, language_version))): + yield + + +def install_environment( + prefix: Prefix, version: str, additional_dependencies: Sequence[str], +) -> None: + helpers.assert_version_default('perl', version) + + with clean_path_on_failure(_envdir(prefix, version)): + with in_env(prefix, version): + helpers.run_setup_cmd( + prefix, ('cpan', '-T', '.', *additional_dependencies), + ) + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: + with in_env(hook.prefix, hook.language_version): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/pygrep.py b/pre_commit/languages/pygrep.py new file mode 100644 index 0000000..40adba0 --- /dev/null +++ b/pre_commit/languages/pygrep.py @@ -0,0 +1,87 @@ +import argparse +import re +import sys +from typing import Optional +from typing import Pattern +from typing import Sequence +from typing import Tuple + +from pre_commit import output +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.xargs import xargs + +ENVIRONMENT_DIR = None +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy +install_environment = helpers.no_install + + +def _process_filename_by_line(pattern: Pattern[bytes], filename: str) -> int: + retv = 0 + with open(filename, 'rb') as f: + for line_no, line in enumerate(f, start=1): + if pattern.search(line): + retv = 1 + output.write(f'{filename}:{line_no}:') + output.write_line_b(line.rstrip(b'\r\n')) + return retv + + +def _process_filename_at_once(pattern: Pattern[bytes], filename: str) -> int: + retv = 0 + with open(filename, 'rb') as f: + contents = f.read() + match = pattern.search(contents) + if match: + retv = 1 + line_no = contents[:match.start()].count(b'\n') + output.write(f'{filename}:{line_no + 1}:') + + matched_lines = match[0].split(b'\n') + matched_lines[0] = contents.split(b'\n')[line_no] + + output.write_line_b(b'\n'.join(matched_lines)) + return retv + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: + exe = (sys.executable, '-m', __name__) + tuple(hook.args) + (hook.entry,) + return xargs(exe, file_args, color=color) + + +def main(argv: Optional[Sequence[str]] = None) -> int: + parser = argparse.ArgumentParser( + description=( + 'grep-like finder using python regexes. Unlike grep, this tool ' + 'returns nonzero when it finds a match and zero otherwise. The ' + 'idea here being that matches are "problems".' + ), + ) + parser.add_argument('-i', '--ignore-case', action='store_true') + parser.add_argument('--multiline', action='store_true') + parser.add_argument('pattern', help='python regex pattern.') + parser.add_argument('filenames', nargs='*') + args = parser.parse_args(argv) + + flags = re.IGNORECASE if args.ignore_case else 0 + if args.multiline: + flags |= re.MULTILINE | re.DOTALL + + pattern = re.compile(args.pattern.encode(), flags) + + retv = 0 + for filename in args.filenames: + if args.multiline: + retv |= _process_filename_at_once(pattern, filename) + else: + retv |= _process_filename_by_line(pattern, filename) + return retv + + +if __name__ == '__main__': + exit(main()) diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py new file mode 100644 index 0000000..5073a8b --- /dev/null +++ b/pre_commit/languages/python.py @@ -0,0 +1,210 @@ +import contextlib +import functools +import os +import sys +from typing import Callable +from typing import ContextManager +from typing import Generator +from typing import Optional +from typing import Sequence +from typing import Tuple + +import pre_commit.constants as C +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import UNSET +from pre_commit.envcontext import Var +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.parse_shebang import find_executable +from pre_commit.prefix import Prefix +from pre_commit.util import CalledProcessError +from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b + +ENVIRONMENT_DIR = 'py_env' + + +def bin_dir(venv: str) -> str: + """On windows there's a different directory for the virtualenv""" + bin_part = 'Scripts' if os.name == 'nt' else 'bin' + return os.path.join(venv, bin_part) + + +def get_env_patch(venv: str) -> PatchesT: + return ( + ('PYTHONHOME', UNSET), + ('VIRTUAL_ENV', venv), + ('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))), + ) + + +def _find_by_py_launcher( + version: str, +) -> Optional[str]: # pragma: no cover (windows only) + if version.startswith('python'): + num = version[len('python'):] + try: + cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)') + return cmd_output(*cmd)[1].strip() + except CalledProcessError: + pass + return None + + +def _find_by_sys_executable() -> Optional[str]: + def _norm(path: str) -> Optional[str]: + _, exe = os.path.split(path.lower()) + exe, _, _ = exe.partition('.exe') + if exe not in {'python', 'pythonw'} and find_executable(exe): + return exe + return None + + # On linux, I see these common sys.executables: + # + # system `python`: /usr/bin/python -> python2.7 + # system `python2`: /usr/bin/python2 -> python2.7 + # virtualenv v: v/bin/python (will not return from this loop) + # virtualenv v -ppython2: v/bin/python -> python2 + # virtualenv v -ppython2.7: v/bin/python -> python2.7 + # virtualenv v -ppypy: v/bin/python -> v/bin/pypy + for path in (sys.executable, os.path.realpath(sys.executable)): + exe = _norm(path) + if exe: + return exe + return None + + +@functools.lru_cache(maxsize=1) +def get_default_version() -> str: # pragma: no cover (platform dependent) + # First attempt from `sys.executable` (or the realpath) + exe = _find_by_sys_executable() + if exe: + return exe + + # Next try the `pythonX.X` executable + exe = f'python{sys.version_info[0]}.{sys.version_info[1]}' + if find_executable(exe): + return exe + + if _find_by_py_launcher(exe): + return exe + + # Give a best-effort try for windows + default_folder_name = exe.replace('.', '') + if os.path.exists(fr'C:\{default_folder_name}\python.exe'): + return exe + + # We tried! + return C.DEFAULT + + +def _sys_executable_matches(version: str) -> bool: + if version == 'python': + return True + elif not version.startswith('python'): + return False + + try: + info = tuple(int(p) for p in version[len('python'):].split('.')) + except ValueError: + return False + + return sys.version_info[:len(info)] == info + + +def norm_version(version: str) -> str: + # first see if our current executable is appropriate + if _sys_executable_matches(version): + return sys.executable + + if os.name == 'nt': # pragma: no cover (windows) + version_exec = _find_by_py_launcher(version) + if version_exec: + return version_exec + + # Try looking up by name + version_exec = find_executable(version) + if version_exec and version_exec != version: + return version_exec + + # If it is in the form pythonx.x search in the default + # place on windows + if version.startswith('python'): + default_folder_name = version.replace('.', '') + return fr'C:\{default_folder_name}\python.exe' + + # Otherwise assume it is a path + return os.path.expanduser(version) + + +def py_interface( + _dir: str, + _make_venv: Callable[[str, str], None], +) -> Tuple[ + Callable[[Prefix, str], ContextManager[None]], + Callable[[Prefix, str], bool], + Callable[[Hook, Sequence[str], bool], Tuple[int, bytes]], + Callable[[Prefix, str, Sequence[str]], None], +]: + @contextlib.contextmanager + def in_env( + prefix: Prefix, + language_version: str, + ) -> Generator[None, None, None]: + envdir = prefix.path(helpers.environment_dir(_dir, language_version)) + with envcontext(get_env_patch(envdir)): + yield + + def healthy(prefix: Prefix, language_version: str) -> bool: + envdir = helpers.environment_dir(_dir, language_version) + exe_name = 'python.exe' if sys.platform == 'win32' else 'python' + py_exe = prefix.path(bin_dir(envdir), exe_name) + with in_env(prefix, language_version): + retcode, _, _ = cmd_output_b( + py_exe, '-c', 'import ctypes, datetime, io, os, ssl, weakref', + cwd='/', + retcode=None, + ) + return retcode == 0 + + def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, + ) -> Tuple[int, bytes]: + with in_env(hook.prefix, hook.language_version): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) + + def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], + ) -> None: + additional_dependencies = tuple(additional_dependencies) + directory = helpers.environment_dir(_dir, version) + + env_dir = prefix.path(directory) + with clean_path_on_failure(env_dir): + if version != C.DEFAULT: + python = norm_version(version) + else: + python = os.path.realpath(sys.executable) + _make_venv(env_dir, python) + with in_env(prefix, version): + helpers.run_setup_cmd( + prefix, ('pip', 'install', '.') + additional_dependencies, + ) + + return in_env, healthy, run_hook, install_environment + + +def make_venv(envdir: str, python: str) -> None: + env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1') + cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python) + cmd_output_b(*cmd, env=env, cwd='/') + + +_interface = py_interface(ENVIRONMENT_DIR, make_venv) +in_env, healthy, run_hook, install_environment = _interface diff --git a/pre_commit/languages/python_venv.py b/pre_commit/languages/python_venv.py new file mode 100644 index 0000000..5404c8b --- /dev/null +++ b/pre_commit/languages/python_venv.py @@ -0,0 +1,46 @@ +import os.path + +from pre_commit.languages import python +from pre_commit.util import CalledProcessError +from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b + +ENVIRONMENT_DIR = 'py_venv' +get_default_version = python.get_default_version + + +def orig_py_exe(exe: str) -> str: # pragma: no cover (platform specific) + """A -mvenv virtualenv made from a -mvirtualenv virtualenv installs + packages to the incorrect location. Attempt to find the _original_ exe + and invoke `-mvenv` from there. + + See: + - https://github.com/pre-commit/pre-commit/issues/755 + - https://github.com/pypa/virtualenv/issues/1095 + - https://bugs.python.org/issue30811 + """ + try: + prefix_script = 'import sys; print(sys.real_prefix)' + _, prefix, _ = cmd_output(exe, '-c', prefix_script) + prefix = prefix.strip() + except CalledProcessError: + # not created from -mvirtualenv + return exe + + if os.name == 'nt': + expected = os.path.join(prefix, 'python.exe') + else: + expected = os.path.join(prefix, 'bin', os.path.basename(exe)) + + if os.path.exists(expected): + return expected + else: + return exe + + +def make_venv(envdir: str, python: str) -> None: + cmd_output_b(orig_py_exe(python), '-mvenv', envdir, cwd='/') + + +_interface = python.py_interface(ENVIRONMENT_DIR, make_venv) +in_env, healthy, run_hook, install_environment = _interface diff --git a/pre_commit/languages/ruby.py b/pre_commit/languages/ruby.py new file mode 100644 index 0000000..61241f8 --- /dev/null +++ b/pre_commit/languages/ruby.py @@ -0,0 +1,126 @@ +import contextlib +import os.path +import shutil +import tarfile +from typing import Generator +from typing import Sequence +from typing import Tuple + +import pre_commit.constants as C +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import Var +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.prefix import Prefix +from pre_commit.util import CalledProcessError +from pre_commit.util import clean_path_on_failure +from pre_commit.util import resource_bytesio + +ENVIRONMENT_DIR = 'rbenv' +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def get_env_patch( + venv: str, + language_version: str, +) -> PatchesT: # pragma: win32 no cover + patches: PatchesT = ( + ('GEM_HOME', os.path.join(venv, 'gems')), + ('RBENV_ROOT', venv), + ('BUNDLE_IGNORE_CONFIG', '1'), + ( + 'PATH', ( + os.path.join(venv, 'gems', 'bin'), os.pathsep, + os.path.join(venv, 'shims'), os.pathsep, + os.path.join(venv, 'bin'), os.pathsep, Var('PATH'), + ), + ), + ) + if language_version != C.DEFAULT: + patches += (('RBENV_VERSION', language_version),) + return patches + + +@contextlib.contextmanager # pragma: win32 no cover +def in_env( + prefix: Prefix, + language_version: str, +) -> Generator[None, None, None]: + envdir = prefix.path( + helpers.environment_dir(ENVIRONMENT_DIR, language_version), + ) + with envcontext(get_env_patch(envdir, language_version)): + yield + + +def _extract_resource(filename: str, dest: str) -> None: + with resource_bytesio(filename) as bio: + with tarfile.open(fileobj=bio) as tf: + tf.extractall(dest) + + +def _install_rbenv( + prefix: Prefix, + version: str = C.DEFAULT, +) -> None: # pragma: win32 no cover + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + + _extract_resource('rbenv.tar.gz', prefix.path('.')) + shutil.move(prefix.path('rbenv'), prefix.path(directory)) + + # Only install ruby-build if the version is specified + if version != C.DEFAULT: + plugins_dir = prefix.path(directory, 'plugins') + _extract_resource('ruby-download.tar.gz', plugins_dir) + _extract_resource('ruby-build.tar.gz', plugins_dir) + + +def _install_ruby( + prefix: Prefix, + version: str, +) -> None: # pragma: win32 no cover + try: + helpers.run_setup_cmd(prefix, ('rbenv', 'download', version)) + except CalledProcessError: # pragma: no cover (usually find with download) + # Failed to download from mirror for some reason, build it instead + helpers.run_setup_cmd(prefix, ('rbenv', 'install', version)) + + +def install_environment( + prefix: Prefix, version: str, additional_dependencies: Sequence[str], +) -> None: # pragma: win32 no cover + additional_dependencies = tuple(additional_dependencies) + directory = helpers.environment_dir(ENVIRONMENT_DIR, version) + with clean_path_on_failure(prefix.path(directory)): + # TODO: this currently will fail if there's no version specified and + # there's no system ruby installed. Is this ok? + _install_rbenv(prefix, version=version) + with in_env(prefix, version): + # Need to call this before installing so rbenv's directories are + # set up + helpers.run_setup_cmd(prefix, ('rbenv', 'init', '-')) + if version != C.DEFAULT: + _install_ruby(prefix, version) + # Need to call this after installing to set up the shims + helpers.run_setup_cmd(prefix, ('rbenv', 'rehash')) + helpers.run_setup_cmd( + prefix, ('gem', 'build', *prefix.star('.gemspec')), + ) + helpers.run_setup_cmd( + prefix, + ( + 'gem', 'install', '--no-document', + *prefix.star('.gem'), *additional_dependencies, + ), + ) + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: # pragma: win32 no cover + with in_env(hook.prefix, hook.language_version): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/rust.py b/pre_commit/languages/rust.py new file mode 100644 index 0000000..7ea3f54 --- /dev/null +++ b/pre_commit/languages/rust.py @@ -0,0 +1,106 @@ +import contextlib +import os.path +from typing import Generator +from typing import Sequence +from typing import Set +from typing import Tuple + +import toml + +import pre_commit.constants as C +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import Var +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.prefix import Prefix +from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output_b + +ENVIRONMENT_DIR = 'rustenv' +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy + + +def get_env_patch(target_dir: str) -> PatchesT: + return ( + ('PATH', (os.path.join(target_dir, 'bin'), os.pathsep, Var('PATH'))), + ) + + +@contextlib.contextmanager +def in_env(prefix: Prefix) -> Generator[None, None, None]: + target_dir = prefix.path( + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), + ) + with envcontext(get_env_patch(target_dir)): + yield + + +def _add_dependencies( + cargo_toml_path: str, + additional_dependencies: Set[str], +) -> None: + with open(cargo_toml_path, 'r+') as f: + cargo_toml = toml.load(f) + cargo_toml.setdefault('dependencies', {}) + for dep in additional_dependencies: + name, _, spec = dep.partition(':') + cargo_toml['dependencies'][name] = spec or '*' + f.seek(0) + toml.dump(cargo_toml, f) + f.truncate() + + +def install_environment( + prefix: Prefix, + version: str, + additional_dependencies: Sequence[str], +) -> None: + helpers.assert_version_default('rust', version) + directory = prefix.path( + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), + ) + + # There are two cases where we might want to specify more dependencies: + # as dependencies for the library being built, and as binary packages + # to be `cargo install`'d. + # + # Unlike e.g. Python, if we just `cargo install` a library, it won't be + # used for compilation. And if we add a crate providing a binary to the + # `Cargo.toml`, the binary won't be built. + # + # Because of this, we allow specifying "cli" dependencies by prefixing + # with 'cli:'. + cli_deps = { + dep for dep in additional_dependencies if dep.startswith('cli:') + } + lib_deps = set(additional_dependencies) - cli_deps + + if len(lib_deps) > 0: + _add_dependencies(prefix.path('Cargo.toml'), lib_deps) + + with clean_path_on_failure(directory): + packages_to_install: Set[Tuple[str, ...]] = {('--path', '.')} + for cli_dep in cli_deps: + cli_dep = cli_dep[len('cli:'):] + package, _, version = cli_dep.partition(':') + if version != '': + packages_to_install.add((package, '--version', version)) + else: + packages_to_install.add((package,)) + + for args in packages_to_install: + cmd_output_b( + 'cargo', 'install', '--bins', '--root', directory, *args, + cwd=prefix.prefix_dir, + ) + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: + with in_env(hook.prefix): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py new file mode 100644 index 0000000..a5e1365 --- /dev/null +++ b/pre_commit/languages/script.py @@ -0,0 +1,19 @@ +from typing import Sequence +from typing import Tuple + +from pre_commit.hook import Hook +from pre_commit.languages import helpers + +ENVIRONMENT_DIR = None +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy +install_environment = helpers.no_install + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: + cmd = (hook.prefix.path(hook.cmd[0]), *hook.cmd[1:]) + return helpers.run_xargs(hook, cmd, file_args, color=color) diff --git a/pre_commit/languages/swift.py b/pre_commit/languages/swift.py new file mode 100644 index 0000000..66aadc8 --- /dev/null +++ b/pre_commit/languages/swift.py @@ -0,0 +1,64 @@ +import contextlib +import os +from typing import Generator +from typing import Sequence +from typing import Tuple + +import pre_commit.constants as C +from pre_commit.envcontext import envcontext +from pre_commit.envcontext import PatchesT +from pre_commit.envcontext import Var +from pre_commit.hook import Hook +from pre_commit.languages import helpers +from pre_commit.prefix import Prefix +from pre_commit.util import clean_path_on_failure +from pre_commit.util import cmd_output_b + +ENVIRONMENT_DIR = 'swift_env' +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy +BUILD_DIR = '.build' +BUILD_CONFIG = 'release' + + +def get_env_patch(venv: str) -> PatchesT: # pragma: win32 no cover + bin_path = os.path.join(venv, BUILD_DIR, BUILD_CONFIG) + return (('PATH', (bin_path, os.pathsep, Var('PATH'))),) + + +@contextlib.contextmanager # pragma: win32 no cover +def in_env(prefix: Prefix) -> Generator[None, None, None]: + envdir = prefix.path( + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), + ) + with envcontext(get_env_patch(envdir)): + yield + + +def install_environment( + prefix: Prefix, version: str, additional_dependencies: Sequence[str], +) -> None: # pragma: win32 no cover + helpers.assert_version_default('swift', version) + helpers.assert_no_additional_deps('swift', additional_dependencies) + directory = prefix.path( + helpers.environment_dir(ENVIRONMENT_DIR, C.DEFAULT), + ) + + # Build the swift package + with clean_path_on_failure(directory): + os.mkdir(directory) + cmd_output_b( + 'swift', 'build', + '-C', prefix.prefix_dir, + '-c', BUILD_CONFIG, + '--build-path', os.path.join(directory, BUILD_DIR), + ) + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: # pragma: win32 no cover + with in_env(hook.prefix): + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py new file mode 100644 index 0000000..139f45d --- /dev/null +++ b/pre_commit/languages/system.py @@ -0,0 +1,19 @@ +from typing import Sequence +from typing import Tuple + +from pre_commit.hook import Hook +from pre_commit.languages import helpers + + +ENVIRONMENT_DIR = None +get_default_version = helpers.basic_get_default_version +healthy = helpers.basic_healthy +install_environment = helpers.no_install + + +def run_hook( + hook: Hook, + file_args: Sequence[str], + color: bool, +) -> Tuple[int, bytes]: + return helpers.run_xargs(hook, hook.cmd, file_args, color=color) |