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.
Files changed (91) hide show
  1. pre_commit/__init__.py +0 -0
  2. pre_commit/__main__.py +7 -0
  3. pre_commit/all_languages.py +50 -0
  4. pre_commit/clientlib.py +551 -0
  5. pre_commit/color.py +109 -0
  6. pre_commit/commands/__init__.py +0 -0
  7. pre_commit/commands/autoupdate.py +215 -0
  8. pre_commit/commands/clean.py +16 -0
  9. pre_commit/commands/gc.py +98 -0
  10. pre_commit/commands/hazmat.py +95 -0
  11. pre_commit/commands/hook_impl.py +272 -0
  12. pre_commit/commands/init_templatedir.py +39 -0
  13. pre_commit/commands/install_uninstall.py +167 -0
  14. pre_commit/commands/migrate_config.py +135 -0
  15. pre_commit/commands/run.py +474 -0
  16. pre_commit/commands/sample_config.py +18 -0
  17. pre_commit/commands/try_repo.py +77 -0
  18. pre_commit/commands/validate_config.py +18 -0
  19. pre_commit/commands/validate_manifest.py +18 -0
  20. pre_commit/constants.py +13 -0
  21. pre_commit/envcontext.py +62 -0
  22. pre_commit/error_handler.py +81 -0
  23. pre_commit/errors.py +5 -0
  24. pre_commit/file_lock.py +75 -0
  25. pre_commit/git.py +245 -0
  26. pre_commit/hook.py +60 -0
  27. pre_commit/lang_base.py +196 -0
  28. pre_commit/languages/__init__.py +0 -0
  29. pre_commit/languages/conda.py +77 -0
  30. pre_commit/languages/coursier.py +76 -0
  31. pre_commit/languages/dart.py +97 -0
  32. pre_commit/languages/docker.py +181 -0
  33. pre_commit/languages/docker_image.py +32 -0
  34. pre_commit/languages/dotnet.py +111 -0
  35. pre_commit/languages/fail.py +27 -0
  36. pre_commit/languages/golang.py +161 -0
  37. pre_commit/languages/haskell.py +56 -0
  38. pre_commit/languages/julia.py +133 -0
  39. pre_commit/languages/lua.py +75 -0
  40. pre_commit/languages/node.py +110 -0
  41. pre_commit/languages/perl.py +50 -0
  42. pre_commit/languages/pygrep.py +133 -0
  43. pre_commit/languages/python.py +228 -0
  44. pre_commit/languages/r.py +278 -0
  45. pre_commit/languages/ruby.py +145 -0
  46. pre_commit/languages/rust.py +160 -0
  47. pre_commit/languages/swift.py +50 -0
  48. pre_commit/languages/unsupported.py +10 -0
  49. pre_commit/languages/unsupported_script.py +32 -0
  50. pre_commit/logging_handler.py +42 -0
  51. pre_commit/main.py +472 -0
  52. pre_commit/meta_hooks/__init__.py +0 -0
  53. pre_commit/meta_hooks/check_hooks_apply.py +43 -0
  54. pre_commit/meta_hooks/check_useless_excludes.py +83 -0
  55. pre_commit/meta_hooks/identity.py +17 -0
  56. pre_commit/output.py +33 -0
  57. pre_commit/parse_shebang.py +85 -0
  58. pre_commit/prefix.py +18 -0
  59. pre_commit/repository.py +237 -0
  60. pre_commit/resources/__init__.py +0 -0
  61. pre_commit/resources/empty_template_.npmignore +1 -0
  62. pre_commit/resources/empty_template_Cargo.toml +7 -0
  63. pre_commit/resources/empty_template_LICENSE.renv +7 -0
  64. pre_commit/resources/empty_template_Makefile.PL +6 -0
  65. pre_commit/resources/empty_template_activate.R +440 -0
  66. pre_commit/resources/empty_template_environment.yml +9 -0
  67. pre_commit/resources/empty_template_go.mod +1 -0
  68. pre_commit/resources/empty_template_main.go +3 -0
  69. pre_commit/resources/empty_template_main.rs +1 -0
  70. pre_commit/resources/empty_template_package.json +4 -0
  71. pre_commit/resources/empty_template_pre-commit-package-dev-1.rockspec +12 -0
  72. pre_commit/resources/empty_template_pre_commit_placeholder_package.gemspec +6 -0
  73. pre_commit/resources/empty_template_pubspec.yaml +4 -0
  74. pre_commit/resources/empty_template_renv.lock +20 -0
  75. pre_commit/resources/empty_template_setup.py +4 -0
  76. pre_commit/resources/hook-tmpl +20 -0
  77. pre_commit/resources/rbenv.tar.gz +0 -0
  78. pre_commit/resources/ruby-build.tar.gz +0 -0
  79. pre_commit/resources/ruby-download.tar.gz +0 -0
  80. pre_commit/staged_files_only.py +113 -0
  81. pre_commit/store.py +235 -0
  82. pre_commit/util.py +239 -0
  83. pre_commit/xargs.py +184 -0
  84. pre_commit/yaml.py +19 -0
  85. pre_commit/yaml_rewrite.py +52 -0
  86. pre_commit_ex-4.5.1.dist-info/METADATA +63 -0
  87. pre_commit_ex-4.5.1.dist-info/RECORD +91 -0
  88. pre_commit_ex-4.5.1.dist-info/WHEEL +6 -0
  89. pre_commit_ex-4.5.1.dist-info/entry_points.txt +2 -0
  90. pre_commit_ex-4.5.1.dist-info/licenses/LICENSE +19 -0
  91. 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
+ )