pre-commit-ex 4.5.1__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pre_commit/__init__.py +0 -0
- pre_commit/__main__.py +7 -0
- pre_commit/all_languages.py +50 -0
- pre_commit/clientlib.py +551 -0
- pre_commit/color.py +109 -0
- pre_commit/commands/__init__.py +0 -0
- pre_commit/commands/autoupdate.py +215 -0
- pre_commit/commands/clean.py +16 -0
- pre_commit/commands/gc.py +98 -0
- pre_commit/commands/hazmat.py +95 -0
- pre_commit/commands/hook_impl.py +272 -0
- pre_commit/commands/init_templatedir.py +39 -0
- pre_commit/commands/install_uninstall.py +167 -0
- pre_commit/commands/migrate_config.py +135 -0
- pre_commit/commands/run.py +474 -0
- pre_commit/commands/sample_config.py +18 -0
- pre_commit/commands/try_repo.py +77 -0
- pre_commit/commands/validate_config.py +18 -0
- pre_commit/commands/validate_manifest.py +18 -0
- pre_commit/constants.py +13 -0
- pre_commit/envcontext.py +62 -0
- pre_commit/error_handler.py +81 -0
- pre_commit/errors.py +5 -0
- pre_commit/file_lock.py +75 -0
- pre_commit/git.py +245 -0
- pre_commit/hook.py +60 -0
- pre_commit/lang_base.py +196 -0
- pre_commit/languages/__init__.py +0 -0
- pre_commit/languages/conda.py +77 -0
- pre_commit/languages/coursier.py +76 -0
- pre_commit/languages/dart.py +97 -0
- pre_commit/languages/docker.py +181 -0
- pre_commit/languages/docker_image.py +32 -0
- pre_commit/languages/dotnet.py +111 -0
- pre_commit/languages/fail.py +27 -0
- pre_commit/languages/golang.py +161 -0
- pre_commit/languages/haskell.py +56 -0
- pre_commit/languages/julia.py +133 -0
- pre_commit/languages/lua.py +75 -0
- pre_commit/languages/node.py +110 -0
- pre_commit/languages/perl.py +50 -0
- pre_commit/languages/pygrep.py +133 -0
- pre_commit/languages/python.py +228 -0
- pre_commit/languages/r.py +278 -0
- pre_commit/languages/ruby.py +145 -0
- pre_commit/languages/rust.py +160 -0
- pre_commit/languages/swift.py +50 -0
- pre_commit/languages/unsupported.py +10 -0
- pre_commit/languages/unsupported_script.py +32 -0
- pre_commit/logging_handler.py +42 -0
- pre_commit/main.py +472 -0
- pre_commit/meta_hooks/__init__.py +0 -0
- pre_commit/meta_hooks/check_hooks_apply.py +43 -0
- pre_commit/meta_hooks/check_useless_excludes.py +83 -0
- pre_commit/meta_hooks/identity.py +17 -0
- pre_commit/output.py +33 -0
- pre_commit/parse_shebang.py +85 -0
- pre_commit/prefix.py +18 -0
- pre_commit/repository.py +237 -0
- pre_commit/resources/__init__.py +0 -0
- pre_commit/resources/empty_template_.npmignore +1 -0
- pre_commit/resources/empty_template_Cargo.toml +7 -0
- pre_commit/resources/empty_template_LICENSE.renv +7 -0
- pre_commit/resources/empty_template_Makefile.PL +6 -0
- pre_commit/resources/empty_template_activate.R +440 -0
- pre_commit/resources/empty_template_environment.yml +9 -0
- pre_commit/resources/empty_template_go.mod +1 -0
- pre_commit/resources/empty_template_main.go +3 -0
- pre_commit/resources/empty_template_main.rs +1 -0
- pre_commit/resources/empty_template_package.json +4 -0
- pre_commit/resources/empty_template_pre-commit-package-dev-1.rockspec +12 -0
- pre_commit/resources/empty_template_pre_commit_placeholder_package.gemspec +6 -0
- pre_commit/resources/empty_template_pubspec.yaml +4 -0
- pre_commit/resources/empty_template_renv.lock +20 -0
- pre_commit/resources/empty_template_setup.py +4 -0
- pre_commit/resources/hook-tmpl +20 -0
- pre_commit/resources/rbenv.tar.gz +0 -0
- pre_commit/resources/ruby-build.tar.gz +0 -0
- pre_commit/resources/ruby-download.tar.gz +0 -0
- pre_commit/staged_files_only.py +113 -0
- pre_commit/store.py +235 -0
- pre_commit/util.py +239 -0
- pre_commit/xargs.py +184 -0
- pre_commit/yaml.py +19 -0
- pre_commit/yaml_rewrite.py +52 -0
- pre_commit_ex-4.5.1.dist-info/METADATA +63 -0
- pre_commit_ex-4.5.1.dist-info/RECORD +91 -0
- pre_commit_ex-4.5.1.dist-info/WHEEL +6 -0
- pre_commit_ex-4.5.1.dist-info/entry_points.txt +2 -0
- pre_commit_ex-4.5.1.dist-info/licenses/LICENSE +19 -0
- pre_commit_ex-4.5.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
from re import Pattern
|
|
8
|
+
from typing import NamedTuple
|
|
9
|
+
|
|
10
|
+
from pre_commit import lang_base
|
|
11
|
+
from pre_commit import output
|
|
12
|
+
from pre_commit.prefix import Prefix
|
|
13
|
+
from pre_commit.xargs import xargs
|
|
14
|
+
|
|
15
|
+
ENVIRONMENT_DIR = None
|
|
16
|
+
get_default_version = lang_base.basic_get_default_version
|
|
17
|
+
health_check = lang_base.basic_health_check
|
|
18
|
+
install_environment = lang_base.no_install
|
|
19
|
+
in_env = lang_base.no_env
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _process_filename_by_line(pattern: Pattern[bytes], filename: str) -> int:
|
|
23
|
+
retv = 0
|
|
24
|
+
with open(filename, 'rb') as f:
|
|
25
|
+
for line_no, line in enumerate(f, start=1):
|
|
26
|
+
if pattern.search(line):
|
|
27
|
+
retv = 1
|
|
28
|
+
output.write(f'{filename}:{line_no}:')
|
|
29
|
+
output.write_line_b(line.rstrip(b'\r\n'))
|
|
30
|
+
return retv
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _process_filename_at_once(pattern: Pattern[bytes], filename: str) -> int:
|
|
34
|
+
retv = 0
|
|
35
|
+
with open(filename, 'rb') as f:
|
|
36
|
+
contents = f.read()
|
|
37
|
+
match = pattern.search(contents)
|
|
38
|
+
if match:
|
|
39
|
+
retv = 1
|
|
40
|
+
line_no = contents[:match.start()].count(b'\n')
|
|
41
|
+
output.write(f'{filename}:{line_no + 1}:')
|
|
42
|
+
|
|
43
|
+
matched_lines = match[0].split(b'\n')
|
|
44
|
+
matched_lines[0] = contents.split(b'\n')[line_no]
|
|
45
|
+
|
|
46
|
+
output.write_line_b(b'\n'.join(matched_lines))
|
|
47
|
+
return retv
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _process_filename_by_line_negated(
|
|
51
|
+
pattern: Pattern[bytes],
|
|
52
|
+
filename: str,
|
|
53
|
+
) -> int:
|
|
54
|
+
with open(filename, 'rb') as f:
|
|
55
|
+
for line in f:
|
|
56
|
+
if pattern.search(line):
|
|
57
|
+
return 0
|
|
58
|
+
else:
|
|
59
|
+
output.write_line(filename)
|
|
60
|
+
return 1
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _process_filename_at_once_negated(
|
|
64
|
+
pattern: Pattern[bytes],
|
|
65
|
+
filename: str,
|
|
66
|
+
) -> int:
|
|
67
|
+
with open(filename, 'rb') as f:
|
|
68
|
+
contents = f.read()
|
|
69
|
+
match = pattern.search(contents)
|
|
70
|
+
if match:
|
|
71
|
+
return 0
|
|
72
|
+
else:
|
|
73
|
+
output.write_line(filename)
|
|
74
|
+
return 1
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Choice(NamedTuple):
|
|
78
|
+
multiline: bool
|
|
79
|
+
negate: bool
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
FNS = {
|
|
83
|
+
Choice(multiline=True, negate=True): _process_filename_at_once_negated,
|
|
84
|
+
Choice(multiline=True, negate=False): _process_filename_at_once,
|
|
85
|
+
Choice(multiline=False, negate=True): _process_filename_by_line_negated,
|
|
86
|
+
Choice(multiline=False, negate=False): _process_filename_by_line,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def run_hook(
|
|
91
|
+
prefix: Prefix,
|
|
92
|
+
entry: str,
|
|
93
|
+
args: Sequence[str],
|
|
94
|
+
file_args: Sequence[str],
|
|
95
|
+
*,
|
|
96
|
+
is_local: bool,
|
|
97
|
+
require_serial: bool,
|
|
98
|
+
color: bool,
|
|
99
|
+
) -> tuple[int, bytes]:
|
|
100
|
+
cmd = (sys.executable, '-m', __name__, *args, entry)
|
|
101
|
+
return xargs(cmd, file_args, color=color)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
105
|
+
parser = argparse.ArgumentParser(
|
|
106
|
+
description=(
|
|
107
|
+
'grep-like finder using python regexes. Unlike grep, this tool '
|
|
108
|
+
'returns nonzero when it finds a match and zero otherwise. The '
|
|
109
|
+
'idea here being that matches are "problems".'
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
parser.add_argument('-i', '--ignore-case', action='store_true')
|
|
113
|
+
parser.add_argument('--multiline', action='store_true')
|
|
114
|
+
parser.add_argument('--negate', action='store_true')
|
|
115
|
+
parser.add_argument('pattern', help='python regex pattern.')
|
|
116
|
+
parser.add_argument('filenames', nargs='*')
|
|
117
|
+
args = parser.parse_args(argv)
|
|
118
|
+
|
|
119
|
+
flags = re.IGNORECASE if args.ignore_case else 0
|
|
120
|
+
if args.multiline:
|
|
121
|
+
flags |= re.MULTILINE | re.DOTALL
|
|
122
|
+
|
|
123
|
+
pattern = re.compile(args.pattern.encode(), flags)
|
|
124
|
+
|
|
125
|
+
retv = 0
|
|
126
|
+
process_fn = FNS[Choice(multiline=args.multiline, negate=args.negate)]
|
|
127
|
+
for filename in args.filenames:
|
|
128
|
+
retv |= process_fn(pattern, filename)
|
|
129
|
+
return retv
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == '__main__':
|
|
133
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import functools
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Generator
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
|
|
10
|
+
import pre_commit.constants as C
|
|
11
|
+
from pre_commit import lang_base
|
|
12
|
+
from pre_commit.envcontext import envcontext
|
|
13
|
+
from pre_commit.envcontext import PatchesT
|
|
14
|
+
from pre_commit.envcontext import UNSET
|
|
15
|
+
from pre_commit.envcontext import Var
|
|
16
|
+
from pre_commit.parse_shebang import find_executable
|
|
17
|
+
from pre_commit.prefix import Prefix
|
|
18
|
+
from pre_commit.util import CalledProcessError
|
|
19
|
+
from pre_commit.util import cmd_output
|
|
20
|
+
from pre_commit.util import cmd_output_b
|
|
21
|
+
from pre_commit.util import win_exe
|
|
22
|
+
|
|
23
|
+
ENVIRONMENT_DIR = 'py_env'
|
|
24
|
+
run_hook = lang_base.basic_run_hook
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@functools.cache
|
|
28
|
+
def _version_info(exe: str) -> str:
|
|
29
|
+
prog = 'import sys;print(".".join(str(p) for p in sys.version_info))'
|
|
30
|
+
try:
|
|
31
|
+
return cmd_output(exe, '-S', '-c', prog)[1].strip()
|
|
32
|
+
except CalledProcessError:
|
|
33
|
+
return f'<<error retrieving version from {exe}>>'
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _read_pyvenv_cfg(filename: str) -> dict[str, str]:
|
|
37
|
+
ret = {}
|
|
38
|
+
with open(filename, encoding='UTF-8') as f:
|
|
39
|
+
for line in f:
|
|
40
|
+
try:
|
|
41
|
+
k, v = line.split('=')
|
|
42
|
+
except ValueError: # blank line / comment / etc.
|
|
43
|
+
continue
|
|
44
|
+
else:
|
|
45
|
+
ret[k.strip()] = v.strip()
|
|
46
|
+
return ret
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def bin_dir(venv: str) -> str:
|
|
50
|
+
"""On windows there's a different directory for the virtualenv"""
|
|
51
|
+
bin_part = 'Scripts' if sys.platform == 'win32' else 'bin'
|
|
52
|
+
return os.path.join(venv, bin_part)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def get_env_patch(venv: str) -> PatchesT:
|
|
56
|
+
return (
|
|
57
|
+
('PIP_DISABLE_PIP_VERSION_CHECK', '1'),
|
|
58
|
+
('PYTHONHOME', UNSET),
|
|
59
|
+
('VIRTUAL_ENV', venv),
|
|
60
|
+
('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _find_by_py_launcher(
|
|
65
|
+
version: str,
|
|
66
|
+
) -> str | None: # pragma: no cover (windows only)
|
|
67
|
+
if version.startswith('python'):
|
|
68
|
+
num = version.removeprefix('python')
|
|
69
|
+
cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)')
|
|
70
|
+
env = dict(os.environ, PYTHONIOENCODING='UTF-8')
|
|
71
|
+
try:
|
|
72
|
+
return cmd_output(*cmd, env=env)[1].strip()
|
|
73
|
+
except CalledProcessError:
|
|
74
|
+
pass
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _impl_exe_name() -> str:
|
|
79
|
+
if sys.implementation.name == 'cpython': # pragma: cpython cover
|
|
80
|
+
return 'python'
|
|
81
|
+
else: # pragma: cpython no cover
|
|
82
|
+
return sys.implementation.name # pypy mostly
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _find_by_sys_executable() -> str | None:
|
|
86
|
+
def _norm(path: str) -> str | None:
|
|
87
|
+
_, exe = os.path.split(path.lower())
|
|
88
|
+
exe, _, _ = exe.partition('.exe')
|
|
89
|
+
if exe not in {'python', 'pythonw'} and find_executable(exe):
|
|
90
|
+
return exe
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
# On linux, I see these common sys.executables:
|
|
94
|
+
#
|
|
95
|
+
# system `python`: /usr/bin/python -> python2.7
|
|
96
|
+
# system `python2`: /usr/bin/python2 -> python2.7
|
|
97
|
+
# virtualenv v: v/bin/python (will not return from this loop)
|
|
98
|
+
# virtualenv v -ppython2: v/bin/python -> python2
|
|
99
|
+
# virtualenv v -ppython2.7: v/bin/python -> python2.7
|
|
100
|
+
# virtualenv v -ppypy: v/bin/python -> v/bin/pypy
|
|
101
|
+
for path in (sys.executable, os.path.realpath(sys.executable)):
|
|
102
|
+
exe = _norm(path)
|
|
103
|
+
if exe:
|
|
104
|
+
return exe
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@functools.lru_cache(maxsize=1)
|
|
109
|
+
def get_default_version() -> str: # pragma: no cover (platform dependent)
|
|
110
|
+
v_major = f'{sys.version_info[0]}'
|
|
111
|
+
v_minor = f'{sys.version_info[0]}.{sys.version_info[1]}'
|
|
112
|
+
|
|
113
|
+
# attempt the likely implementation exe
|
|
114
|
+
for potential in (v_minor, v_major):
|
|
115
|
+
exe = f'{_impl_exe_name()}{potential}'
|
|
116
|
+
if find_executable(exe):
|
|
117
|
+
return exe
|
|
118
|
+
|
|
119
|
+
# next try `sys.executable` (or the realpath)
|
|
120
|
+
maybe_exe = _find_by_sys_executable()
|
|
121
|
+
if maybe_exe:
|
|
122
|
+
return maybe_exe
|
|
123
|
+
|
|
124
|
+
# maybe on windows we can find it via py launcher?
|
|
125
|
+
if sys.platform == 'win32': # pragma: win32 cover
|
|
126
|
+
exe = f'python{v_minor}'
|
|
127
|
+
if _find_by_py_launcher(exe):
|
|
128
|
+
return exe
|
|
129
|
+
|
|
130
|
+
# We tried!
|
|
131
|
+
return C.DEFAULT
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _sys_executable_matches(version: str) -> bool:
|
|
135
|
+
if version == 'python':
|
|
136
|
+
return True
|
|
137
|
+
elif not version.startswith('python'):
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
info = tuple(int(p) for p in version.removeprefix('python').split('.'))
|
|
142
|
+
except ValueError:
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
return sys.version_info[:len(info)] == info
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def norm_version(version: str) -> str | None:
|
|
149
|
+
if version == C.DEFAULT: # use virtualenv's default
|
|
150
|
+
return None
|
|
151
|
+
elif _sys_executable_matches(version): # virtualenv defaults to our exe
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
if sys.platform == 'win32': # pragma: no cover (windows)
|
|
155
|
+
version_exec = _find_by_py_launcher(version)
|
|
156
|
+
if version_exec:
|
|
157
|
+
return version_exec
|
|
158
|
+
|
|
159
|
+
# Try looking up by name
|
|
160
|
+
version_exec = find_executable(version)
|
|
161
|
+
if version_exec and version_exec != version:
|
|
162
|
+
return version_exec
|
|
163
|
+
|
|
164
|
+
# Otherwise assume it is a path
|
|
165
|
+
return os.path.expanduser(version)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@contextlib.contextmanager
|
|
169
|
+
def in_env(prefix: Prefix, version: str) -> Generator[None]:
|
|
170
|
+
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
|
171
|
+
with envcontext(get_env_patch(envdir)):
|
|
172
|
+
yield
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def health_check(prefix: Prefix, version: str) -> str | None:
|
|
176
|
+
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
|
177
|
+
pyvenv_cfg = os.path.join(envdir, 'pyvenv.cfg')
|
|
178
|
+
|
|
179
|
+
# created with "old" virtualenv
|
|
180
|
+
if not os.path.exists(pyvenv_cfg):
|
|
181
|
+
return 'pyvenv.cfg does not exist (old virtualenv?)'
|
|
182
|
+
|
|
183
|
+
exe_name = win_exe('python')
|
|
184
|
+
py_exe = prefix.path(bin_dir(envdir), exe_name)
|
|
185
|
+
cfg = _read_pyvenv_cfg(pyvenv_cfg)
|
|
186
|
+
|
|
187
|
+
if 'version_info' not in cfg:
|
|
188
|
+
return "created virtualenv's pyvenv.cfg is missing `version_info`"
|
|
189
|
+
|
|
190
|
+
# always use uncached lookup here in case we replaced an unhealthy env
|
|
191
|
+
virtualenv_version = _version_info.__wrapped__(py_exe)
|
|
192
|
+
if virtualenv_version != cfg['version_info']:
|
|
193
|
+
return (
|
|
194
|
+
f'virtualenv python version did not match created version:\n'
|
|
195
|
+
f'- actual version: {virtualenv_version}\n'
|
|
196
|
+
f'- expected version: {cfg["version_info"]}\n'
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# made with an older version of virtualenv? skip `base-executable` check
|
|
200
|
+
if 'base-executable' not in cfg:
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
base_exe_version = _version_info(cfg['base-executable'])
|
|
204
|
+
if base_exe_version != cfg['version_info']:
|
|
205
|
+
return (
|
|
206
|
+
f'base executable python version does not match created version:\n'
|
|
207
|
+
f'- base-executable version: {base_exe_version}\n'
|
|
208
|
+
f'- expected version: {cfg["version_info"]}\n'
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def install_environment(
|
|
215
|
+
prefix: Prefix,
|
|
216
|
+
version: str,
|
|
217
|
+
additional_dependencies: Sequence[str],
|
|
218
|
+
) -> None:
|
|
219
|
+
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
|
220
|
+
venv_cmd = [sys.executable, '-mvirtualenv', envdir]
|
|
221
|
+
python = norm_version(version)
|
|
222
|
+
if python is not None:
|
|
223
|
+
venv_cmd.extend(('-p', python))
|
|
224
|
+
install_cmd = ('python', '-mpip', 'install', '.', *additional_dependencies)
|
|
225
|
+
|
|
226
|
+
cmd_output_b(*venv_cmd, cwd='/')
|
|
227
|
+
with in_env(prefix, version):
|
|
228
|
+
lang_base.setup_cmd(prefix, install_cmd)
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import os
|
|
5
|
+
import shlex
|
|
6
|
+
import shutil
|
|
7
|
+
import tempfile
|
|
8
|
+
import textwrap
|
|
9
|
+
from collections.abc import Generator
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
|
|
12
|
+
from pre_commit import lang_base
|
|
13
|
+
from pre_commit.envcontext import envcontext
|
|
14
|
+
from pre_commit.envcontext import PatchesT
|
|
15
|
+
from pre_commit.envcontext import UNSET
|
|
16
|
+
from pre_commit.prefix import Prefix
|
|
17
|
+
from pre_commit.util import cmd_output
|
|
18
|
+
from pre_commit.util import win_exe
|
|
19
|
+
|
|
20
|
+
ENVIRONMENT_DIR = 'renv'
|
|
21
|
+
get_default_version = lang_base.basic_get_default_version
|
|
22
|
+
|
|
23
|
+
_RENV_ACTIVATED_OPTS = (
|
|
24
|
+
'--no-save', '--no-restore', '--no-site-file', '--no-environ',
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _execute_r(
|
|
29
|
+
code: str, *,
|
|
30
|
+
prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str,
|
|
31
|
+
cli_opts: Sequence[str],
|
|
32
|
+
) -> str:
|
|
33
|
+
with in_env(prefix, version), _r_code_in_tempfile(code) as f:
|
|
34
|
+
_, out, _ = cmd_output(
|
|
35
|
+
_rscript_exec(), *cli_opts, f, *args, cwd=cwd,
|
|
36
|
+
)
|
|
37
|
+
return out.rstrip('\n')
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _execute_r_in_renv(
|
|
41
|
+
code: str, *,
|
|
42
|
+
prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str,
|
|
43
|
+
) -> str:
|
|
44
|
+
return _execute_r(
|
|
45
|
+
code=code, prefix=prefix, version=version, args=args, cwd=cwd,
|
|
46
|
+
cli_opts=_RENV_ACTIVATED_OPTS,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _execute_vanilla_r(
|
|
51
|
+
code: str, *,
|
|
52
|
+
prefix: Prefix, version: str, args: Sequence[str] = (), cwd: str,
|
|
53
|
+
) -> str:
|
|
54
|
+
return _execute_r(
|
|
55
|
+
code=code, prefix=prefix, version=version, args=args, cwd=cwd,
|
|
56
|
+
cli_opts=('--vanilla',),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _read_installed_version(envdir: str, prefix: Prefix, version: str) -> str:
|
|
61
|
+
return _execute_r_in_renv(
|
|
62
|
+
'cat(renv::settings$r.version())',
|
|
63
|
+
prefix=prefix, version=version,
|
|
64
|
+
cwd=envdir,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _read_executable_version(envdir: str, prefix: Prefix, version: str) -> str:
|
|
69
|
+
return _execute_r_in_renv(
|
|
70
|
+
'cat(as.character(getRversion()))',
|
|
71
|
+
prefix=prefix, version=version,
|
|
72
|
+
cwd=envdir,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _write_current_r_version(
|
|
77
|
+
envdir: str, prefix: Prefix, version: str,
|
|
78
|
+
) -> None:
|
|
79
|
+
_execute_r_in_renv(
|
|
80
|
+
'renv::settings$r.version(as.character(getRversion()))',
|
|
81
|
+
prefix=prefix, version=version,
|
|
82
|
+
cwd=envdir,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def health_check(prefix: Prefix, version: str) -> str | None:
|
|
87
|
+
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
|
88
|
+
|
|
89
|
+
r_version_installation = _read_installed_version(
|
|
90
|
+
envdir=envdir, prefix=prefix, version=version,
|
|
91
|
+
)
|
|
92
|
+
r_version_current_executable = _read_executable_version(
|
|
93
|
+
envdir=envdir, prefix=prefix, version=version,
|
|
94
|
+
)
|
|
95
|
+
if r_version_installation in {'NULL', ''}:
|
|
96
|
+
return (
|
|
97
|
+
f'Hooks were installed with an unknown R version. R version for '
|
|
98
|
+
f'hook repo now set to {r_version_current_executable}'
|
|
99
|
+
)
|
|
100
|
+
elif r_version_installation != r_version_current_executable:
|
|
101
|
+
return (
|
|
102
|
+
f'Hooks were installed for R version {r_version_installation}, '
|
|
103
|
+
f'but current R executable has version '
|
|
104
|
+
f'{r_version_current_executable}'
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@contextlib.contextmanager
|
|
111
|
+
def _r_code_in_tempfile(code: str) -> Generator[str]:
|
|
112
|
+
"""
|
|
113
|
+
To avoid quoting and escaping issues, avoid `Rscript [options] -e {expr}`
|
|
114
|
+
but use `Rscript [options] path/to/file_with_expr.R`
|
|
115
|
+
"""
|
|
116
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
117
|
+
fname = os.path.join(tmpdir, 'script.R')
|
|
118
|
+
with open(fname, 'w') as f:
|
|
119
|
+
f.write(_inline_r_setup(textwrap.dedent(code)))
|
|
120
|
+
yield fname
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_env_patch(venv: str) -> PatchesT:
|
|
124
|
+
return (
|
|
125
|
+
('R_PROFILE_USER', os.path.join(venv, 'activate.R')),
|
|
126
|
+
('RENV_PROJECT', UNSET),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@contextlib.contextmanager
|
|
131
|
+
def in_env(prefix: Prefix, version: str) -> Generator[None]:
|
|
132
|
+
envdir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
|
133
|
+
with envcontext(get_env_patch(envdir)):
|
|
134
|
+
yield
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _prefix_if_file_entry(
|
|
138
|
+
entry: list[str],
|
|
139
|
+
prefix: Prefix,
|
|
140
|
+
*,
|
|
141
|
+
is_local: bool,
|
|
142
|
+
) -> Sequence[str]:
|
|
143
|
+
if entry[1] == '-e' or is_local:
|
|
144
|
+
return entry[1:]
|
|
145
|
+
else:
|
|
146
|
+
return (prefix.path(entry[1]),)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _rscript_exec() -> str:
|
|
150
|
+
r_home = os.environ.get('R_HOME')
|
|
151
|
+
if r_home is None:
|
|
152
|
+
return 'Rscript'
|
|
153
|
+
else:
|
|
154
|
+
return os.path.join(r_home, 'bin', win_exe('Rscript'))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _entry_validate(entry: list[str]) -> None:
|
|
158
|
+
"""
|
|
159
|
+
Allowed entries:
|
|
160
|
+
# Rscript -e expr
|
|
161
|
+
# Rscript path/to/file
|
|
162
|
+
"""
|
|
163
|
+
if entry[0] != 'Rscript':
|
|
164
|
+
raise ValueError('entry must start with `Rscript`.')
|
|
165
|
+
|
|
166
|
+
if entry[1] == '-e':
|
|
167
|
+
if len(entry) > 3:
|
|
168
|
+
raise ValueError('You can supply at most one expression.')
|
|
169
|
+
elif len(entry) > 2:
|
|
170
|
+
raise ValueError(
|
|
171
|
+
'The only valid syntax is `Rscript -e {expr}`'
|
|
172
|
+
'or `Rscript path/to/hook/script`',
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _cmd_from_hook(
|
|
177
|
+
prefix: Prefix,
|
|
178
|
+
entry: str,
|
|
179
|
+
args: Sequence[str],
|
|
180
|
+
*,
|
|
181
|
+
is_local: bool,
|
|
182
|
+
) -> tuple[str, ...]:
|
|
183
|
+
cmd = shlex.split(entry)
|
|
184
|
+
_entry_validate(cmd)
|
|
185
|
+
|
|
186
|
+
cmd_part = _prefix_if_file_entry(cmd, prefix, is_local=is_local)
|
|
187
|
+
return (cmd[0], *_RENV_ACTIVATED_OPTS, *cmd_part, *args)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def install_environment(
|
|
191
|
+
prefix: Prefix,
|
|
192
|
+
version: str,
|
|
193
|
+
additional_dependencies: Sequence[str],
|
|
194
|
+
) -> None:
|
|
195
|
+
lang_base.assert_version_default('r', version)
|
|
196
|
+
|
|
197
|
+
env_dir = lang_base.environment_dir(prefix, ENVIRONMENT_DIR, version)
|
|
198
|
+
os.makedirs(env_dir, exist_ok=True)
|
|
199
|
+
shutil.copy(prefix.path('renv.lock'), env_dir)
|
|
200
|
+
shutil.copytree(prefix.path('renv'), os.path.join(env_dir, 'renv'))
|
|
201
|
+
|
|
202
|
+
r_code_inst_environment = f"""\
|
|
203
|
+
prefix_dir <- {prefix.prefix_dir!r}
|
|
204
|
+
options(
|
|
205
|
+
repos = c(CRAN = "https://cran.rstudio.com"),
|
|
206
|
+
renv.consent = TRUE
|
|
207
|
+
)
|
|
208
|
+
source("renv/activate.R")
|
|
209
|
+
renv::restore()
|
|
210
|
+
activate_statement <- paste0(
|
|
211
|
+
'suppressWarnings({{',
|
|
212
|
+
'old <- setwd("', getwd(), '"); ',
|
|
213
|
+
'source("renv/activate.R"); ',
|
|
214
|
+
'setwd(old); ',
|
|
215
|
+
'renv::load("', getwd(), '");}})'
|
|
216
|
+
)
|
|
217
|
+
writeLines(activate_statement, 'activate.R')
|
|
218
|
+
is_package <- tryCatch(
|
|
219
|
+
{{
|
|
220
|
+
path_desc <- file.path(prefix_dir, 'DESCRIPTION')
|
|
221
|
+
suppressWarnings(desc <- read.dcf(path_desc))
|
|
222
|
+
"Package" %in% colnames(desc)
|
|
223
|
+
}},
|
|
224
|
+
error = function(...) FALSE
|
|
225
|
+
)
|
|
226
|
+
if (is_package) {{
|
|
227
|
+
renv::install(prefix_dir)
|
|
228
|
+
}}
|
|
229
|
+
"""
|
|
230
|
+
_execute_vanilla_r(
|
|
231
|
+
r_code_inst_environment,
|
|
232
|
+
prefix=prefix, version=version, cwd=env_dir,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
_write_current_r_version(envdir=env_dir, prefix=prefix, version=version)
|
|
236
|
+
if additional_dependencies:
|
|
237
|
+
r_code_inst_add = 'renv::install(commandArgs(trailingOnly = TRUE))'
|
|
238
|
+
_execute_r_in_renv(
|
|
239
|
+
code=r_code_inst_add, prefix=prefix, version=version,
|
|
240
|
+
args=additional_dependencies,
|
|
241
|
+
cwd=env_dir,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _inline_r_setup(code: str) -> str:
|
|
246
|
+
"""
|
|
247
|
+
Some behaviour of R cannot be configured via env variables, but can
|
|
248
|
+
only be configured via R options once R has started. These are set here.
|
|
249
|
+
"""
|
|
250
|
+
with_option = [
|
|
251
|
+
textwrap.dedent("""\
|
|
252
|
+
options(
|
|
253
|
+
install.packages.compile.from.source = "never",
|
|
254
|
+
pkgType = "binary"
|
|
255
|
+
)
|
|
256
|
+
"""),
|
|
257
|
+
code,
|
|
258
|
+
]
|
|
259
|
+
return '\n'.join(with_option)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def run_hook(
|
|
263
|
+
prefix: Prefix,
|
|
264
|
+
entry: str,
|
|
265
|
+
args: Sequence[str],
|
|
266
|
+
file_args: Sequence[str],
|
|
267
|
+
*,
|
|
268
|
+
is_local: bool,
|
|
269
|
+
require_serial: bool,
|
|
270
|
+
color: bool,
|
|
271
|
+
) -> tuple[int, bytes]:
|
|
272
|
+
cmd = _cmd_from_hook(prefix, entry, args, is_local=is_local)
|
|
273
|
+
return lang_base.run_xargs(
|
|
274
|
+
cmd,
|
|
275
|
+
file_args,
|
|
276
|
+
require_serial=require_serial,
|
|
277
|
+
color=color,
|
|
278
|
+
)
|