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
pre_commit/store.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import logging
|
|
5
|
+
import os.path
|
|
6
|
+
import sqlite3
|
|
7
|
+
import tempfile
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from collections.abc import Generator
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
|
|
12
|
+
import pre_commit.constants as C
|
|
13
|
+
from pre_commit import clientlib
|
|
14
|
+
from pre_commit import file_lock
|
|
15
|
+
from pre_commit import git
|
|
16
|
+
from pre_commit.util import CalledProcessError
|
|
17
|
+
from pre_commit.util import clean_path_on_failure
|
|
18
|
+
from pre_commit.util import cmd_output_b
|
|
19
|
+
from pre_commit.util import resource_text
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger('pre_commit')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _get_default_directory() -> str:
|
|
26
|
+
"""Returns the default directory for the Store. This is intentionally
|
|
27
|
+
underscored to indicate that `Store.get_default_directory` is the intended
|
|
28
|
+
way to get this information. This is also done so
|
|
29
|
+
`Store.get_default_directory` can be mocked in tests and
|
|
30
|
+
`_get_default_directory` can be tested.
|
|
31
|
+
"""
|
|
32
|
+
ret = os.environ.get('PRE_COMMIT_HOME') or os.path.join(
|
|
33
|
+
os.environ.get('XDG_CACHE_HOME') or os.path.expanduser('~/.cache'),
|
|
34
|
+
'pre-commit',
|
|
35
|
+
)
|
|
36
|
+
return os.path.realpath(ret)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_LOCAL_RESOURCES = (
|
|
40
|
+
'Cargo.toml', 'main.go', 'go.mod', 'main.rs', '.npmignore',
|
|
41
|
+
'package.json', 'pre-commit-package-dev-1.rockspec',
|
|
42
|
+
'pre_commit_placeholder_package.gemspec', 'setup.py',
|
|
43
|
+
'environment.yml', 'Makefile.PL', 'pubspec.yaml',
|
|
44
|
+
'renv.lock', 'renv/activate.R', 'renv/LICENSE.renv',
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _make_local_repo(directory: str) -> None:
|
|
49
|
+
for resource in _LOCAL_RESOURCES:
|
|
50
|
+
resource_dirname, resource_basename = os.path.split(resource)
|
|
51
|
+
contents = resource_text(f'empty_template_{resource_basename}')
|
|
52
|
+
target_dir = os.path.join(directory, resource_dirname)
|
|
53
|
+
target_file = os.path.join(target_dir, resource_basename)
|
|
54
|
+
os.makedirs(target_dir, exist_ok=True)
|
|
55
|
+
with open(target_file, 'w') as f:
|
|
56
|
+
f.write(contents)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Store:
|
|
60
|
+
get_default_directory = staticmethod(_get_default_directory)
|
|
61
|
+
|
|
62
|
+
def __init__(self, directory: str | None = None) -> None:
|
|
63
|
+
self.directory = directory or Store.get_default_directory()
|
|
64
|
+
self.db_path = os.path.join(self.directory, 'db.db')
|
|
65
|
+
self.readonly = (
|
|
66
|
+
os.path.exists(self.directory) and
|
|
67
|
+
not os.access(self.directory, os.W_OK)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if not os.path.exists(self.directory):
|
|
71
|
+
os.makedirs(self.directory, exist_ok=True)
|
|
72
|
+
with open(os.path.join(self.directory, 'README'), 'w') as f:
|
|
73
|
+
f.write(
|
|
74
|
+
'This directory is maintained by the pre-commit project.\n'
|
|
75
|
+
'Learn more: https://github.com/pre-commit/pre-commit\n',
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if os.path.exists(self.db_path):
|
|
79
|
+
return
|
|
80
|
+
with self.exclusive_lock():
|
|
81
|
+
# Another process may have already completed this work
|
|
82
|
+
if os.path.exists(self.db_path): # pragma: no cover (race)
|
|
83
|
+
return
|
|
84
|
+
# To avoid a race where someone ^Cs between db creation and
|
|
85
|
+
# execution of the CREATE TABLE statement
|
|
86
|
+
fd, tmpfile = tempfile.mkstemp(dir=self.directory)
|
|
87
|
+
# We'll be managing this file ourselves
|
|
88
|
+
os.close(fd)
|
|
89
|
+
with self.connect(db_path=tmpfile) as db:
|
|
90
|
+
db.executescript(
|
|
91
|
+
'CREATE TABLE repos ('
|
|
92
|
+
' repo TEXT NOT NULL,'
|
|
93
|
+
' ref TEXT NOT NULL,'
|
|
94
|
+
' path TEXT NOT NULL,'
|
|
95
|
+
' PRIMARY KEY (repo, ref)'
|
|
96
|
+
');',
|
|
97
|
+
)
|
|
98
|
+
self._create_configs_table(db)
|
|
99
|
+
|
|
100
|
+
# Atomic file move
|
|
101
|
+
os.replace(tmpfile, self.db_path)
|
|
102
|
+
|
|
103
|
+
@contextlib.contextmanager
|
|
104
|
+
def exclusive_lock(self) -> Generator[None]:
|
|
105
|
+
def blocked_cb() -> None: # pragma: no cover (tests are in-process)
|
|
106
|
+
logger.info('Locking pre-commit directory')
|
|
107
|
+
|
|
108
|
+
with file_lock.lock(os.path.join(self.directory, '.lock'), blocked_cb):
|
|
109
|
+
yield
|
|
110
|
+
|
|
111
|
+
@contextlib.contextmanager
|
|
112
|
+
def connect(
|
|
113
|
+
self,
|
|
114
|
+
db_path: str | None = None,
|
|
115
|
+
) -> Generator[sqlite3.Connection]:
|
|
116
|
+
db_path = db_path or self.db_path
|
|
117
|
+
# sqlite doesn't close its fd with its contextmanager >.<
|
|
118
|
+
# contextlib.closing fixes this.
|
|
119
|
+
# See: https://stackoverflow.com/a/28032829/812183
|
|
120
|
+
with contextlib.closing(sqlite3.connect(db_path)) as db:
|
|
121
|
+
# this creates a transaction
|
|
122
|
+
with db:
|
|
123
|
+
yield db
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def db_repo_name(cls, repo: str, deps: Sequence[str]) -> str:
|
|
127
|
+
if deps:
|
|
128
|
+
return f'{repo}:{",".join(deps)}'
|
|
129
|
+
else:
|
|
130
|
+
return repo
|
|
131
|
+
|
|
132
|
+
def _new_repo(
|
|
133
|
+
self,
|
|
134
|
+
repo: str,
|
|
135
|
+
ref: str,
|
|
136
|
+
deps: Sequence[str],
|
|
137
|
+
make_strategy: Callable[[str], None],
|
|
138
|
+
) -> str:
|
|
139
|
+
original_repo = repo
|
|
140
|
+
repo = self.db_repo_name(repo, deps)
|
|
141
|
+
|
|
142
|
+
def _get_result() -> str | None:
|
|
143
|
+
# Check if we already exist
|
|
144
|
+
with self.connect() as db:
|
|
145
|
+
result = db.execute(
|
|
146
|
+
'SELECT path FROM repos WHERE repo = ? AND ref = ?',
|
|
147
|
+
(repo, ref),
|
|
148
|
+
).fetchone()
|
|
149
|
+
return result[0] if result else None
|
|
150
|
+
|
|
151
|
+
result = _get_result()
|
|
152
|
+
if result:
|
|
153
|
+
return result
|
|
154
|
+
with self.exclusive_lock():
|
|
155
|
+
# Another process may have already completed this work
|
|
156
|
+
result = _get_result()
|
|
157
|
+
if result: # pragma: no cover (race)
|
|
158
|
+
return result
|
|
159
|
+
|
|
160
|
+
logger.info(f'Initializing environment for {repo}.')
|
|
161
|
+
|
|
162
|
+
directory = tempfile.mkdtemp(prefix='repo', dir=self.directory)
|
|
163
|
+
with clean_path_on_failure(directory):
|
|
164
|
+
make_strategy(directory)
|
|
165
|
+
|
|
166
|
+
# Update our db with the created repo
|
|
167
|
+
with self.connect() as db:
|
|
168
|
+
db.execute(
|
|
169
|
+
'INSERT INTO repos (repo, ref, path) VALUES (?, ?, ?)',
|
|
170
|
+
[repo, ref, directory],
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
clientlib.warn_for_stages_on_repo_init(original_repo, directory)
|
|
174
|
+
|
|
175
|
+
return directory
|
|
176
|
+
|
|
177
|
+
def _complete_clone(self, ref: str, git_cmd: Callable[..., None]) -> None:
|
|
178
|
+
"""Perform a complete clone of a repository and its submodules """
|
|
179
|
+
|
|
180
|
+
git_cmd('fetch', 'origin', '--tags')
|
|
181
|
+
git_cmd('checkout', ref)
|
|
182
|
+
git_cmd('submodule', 'update', '--init', '--recursive')
|
|
183
|
+
|
|
184
|
+
def _shallow_clone(self, ref: str, git_cmd: Callable[..., None]) -> None:
|
|
185
|
+
"""Perform a shallow clone of a repository and its submodules """
|
|
186
|
+
|
|
187
|
+
git_config = 'protocol.version=2'
|
|
188
|
+
git_cmd('-c', git_config, 'fetch', 'origin', ref, '--depth=1')
|
|
189
|
+
git_cmd('checkout', 'FETCH_HEAD')
|
|
190
|
+
git_cmd(
|
|
191
|
+
'-c', git_config, 'submodule', 'update', '--init', '--recursive',
|
|
192
|
+
'--depth=1',
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def clone(self, repo: str, ref: str, deps: Sequence[str] = ()) -> str:
|
|
196
|
+
"""Clone the given url and checkout the specific ref."""
|
|
197
|
+
|
|
198
|
+
def clone_strategy(directory: str) -> None:
|
|
199
|
+
git.init_repo(directory, repo)
|
|
200
|
+
env = git.no_git_env()
|
|
201
|
+
|
|
202
|
+
def _git_cmd(*args: str) -> None:
|
|
203
|
+
cmd_output_b('git', *args, cwd=directory, env=env)
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
self._shallow_clone(ref, _git_cmd)
|
|
207
|
+
except CalledProcessError:
|
|
208
|
+
self._complete_clone(ref, _git_cmd)
|
|
209
|
+
|
|
210
|
+
return self._new_repo(repo, ref, deps, clone_strategy)
|
|
211
|
+
|
|
212
|
+
def make_local(self, deps: Sequence[str]) -> str:
|
|
213
|
+
return self._new_repo(
|
|
214
|
+
'local', C.LOCAL_REPO_VERSION, deps, _make_local_repo,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def _create_configs_table(self, db: sqlite3.Connection) -> None:
|
|
218
|
+
db.executescript(
|
|
219
|
+
'CREATE TABLE IF NOT EXISTS configs ('
|
|
220
|
+
' path TEXT NOT NULL,'
|
|
221
|
+
' PRIMARY KEY (path)'
|
|
222
|
+
');',
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
def mark_config_used(self, path: str) -> None:
|
|
226
|
+
if self.readonly: # pragma: win32 no cover
|
|
227
|
+
return
|
|
228
|
+
path = os.path.realpath(path)
|
|
229
|
+
# don't insert config files that do not exist
|
|
230
|
+
if not os.path.exists(path):
|
|
231
|
+
return
|
|
232
|
+
with self.connect() as db:
|
|
233
|
+
# TODO: eventually remove this and only create in _create
|
|
234
|
+
self._create_configs_table(db)
|
|
235
|
+
db.execute('INSERT OR IGNORE INTO configs VALUES (?)', (path,))
|
pre_commit/util.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import errno
|
|
5
|
+
import importlib.resources
|
|
6
|
+
import os.path
|
|
7
|
+
import shutil
|
|
8
|
+
import stat
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from collections.abc import Generator
|
|
13
|
+
from types import TracebackType
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from pre_commit import parse_shebang
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def force_bytes(exc: Any) -> bytes:
|
|
20
|
+
with contextlib.suppress(TypeError):
|
|
21
|
+
return bytes(exc)
|
|
22
|
+
with contextlib.suppress(Exception):
|
|
23
|
+
return str(exc).encode()
|
|
24
|
+
return f'<unprintable {type(exc).__name__} object>'.encode()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@contextlib.contextmanager
|
|
28
|
+
def clean_path_on_failure(path: str) -> Generator[None]:
|
|
29
|
+
"""Cleans up the directory on an exceptional failure."""
|
|
30
|
+
try:
|
|
31
|
+
yield
|
|
32
|
+
except BaseException:
|
|
33
|
+
if os.path.exists(path):
|
|
34
|
+
rmtree(path)
|
|
35
|
+
raise
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def resource_text(filename: str) -> str:
|
|
39
|
+
files = importlib.resources.files('pre_commit.resources')
|
|
40
|
+
return files.joinpath(filename).read_text()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def make_executable(filename: str) -> None:
|
|
44
|
+
original_mode = os.stat(filename).st_mode
|
|
45
|
+
new_mode = original_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
|
|
46
|
+
os.chmod(filename, new_mode)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class CalledProcessError(RuntimeError):
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
returncode: int,
|
|
53
|
+
cmd: tuple[str, ...],
|
|
54
|
+
stdout: bytes,
|
|
55
|
+
stderr: bytes | None,
|
|
56
|
+
) -> None:
|
|
57
|
+
super().__init__(returncode, cmd, stdout, stderr)
|
|
58
|
+
self.returncode = returncode
|
|
59
|
+
self.cmd = cmd
|
|
60
|
+
self.stdout = stdout
|
|
61
|
+
self.stderr = stderr
|
|
62
|
+
|
|
63
|
+
def __bytes__(self) -> bytes:
|
|
64
|
+
def _indent_or_none(part: bytes | None) -> bytes:
|
|
65
|
+
if part:
|
|
66
|
+
return b'\n ' + part.replace(b'\n', b'\n ').rstrip()
|
|
67
|
+
else:
|
|
68
|
+
return b' (none)'
|
|
69
|
+
|
|
70
|
+
return b''.join((
|
|
71
|
+
f'command: {self.cmd!r}\n'.encode(),
|
|
72
|
+
f'return code: {self.returncode}\n'.encode(),
|
|
73
|
+
b'stdout:', _indent_or_none(self.stdout), b'\n',
|
|
74
|
+
b'stderr:', _indent_or_none(self.stderr),
|
|
75
|
+
))
|
|
76
|
+
|
|
77
|
+
def __str__(self) -> str:
|
|
78
|
+
return self.__bytes__().decode()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _setdefault_kwargs(kwargs: dict[str, Any]) -> None:
|
|
82
|
+
for arg in ('stdin', 'stdout', 'stderr'):
|
|
83
|
+
kwargs.setdefault(arg, subprocess.PIPE)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _oserror_to_output(e: OSError) -> tuple[int, bytes, None]:
|
|
87
|
+
return 1, force_bytes(e).rstrip(b'\n') + b'\n', None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def cmd_output_b(
|
|
91
|
+
*cmd: str,
|
|
92
|
+
check: bool = True,
|
|
93
|
+
**kwargs: Any,
|
|
94
|
+
) -> tuple[int, bytes, bytes | None]:
|
|
95
|
+
_setdefault_kwargs(kwargs)
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
cmd = parse_shebang.normalize_cmd(cmd, env=kwargs.get('env'))
|
|
99
|
+
except parse_shebang.ExecutableNotFoundError as e:
|
|
100
|
+
returncode, stdout_b, stderr_b = e.to_output()
|
|
101
|
+
else:
|
|
102
|
+
try:
|
|
103
|
+
proc = subprocess.Popen(cmd, **kwargs)
|
|
104
|
+
except OSError as e:
|
|
105
|
+
returncode, stdout_b, stderr_b = _oserror_to_output(e)
|
|
106
|
+
else:
|
|
107
|
+
stdout_b, stderr_b = proc.communicate()
|
|
108
|
+
returncode = proc.returncode
|
|
109
|
+
|
|
110
|
+
if check and returncode:
|
|
111
|
+
raise CalledProcessError(returncode, cmd, stdout_b, stderr_b)
|
|
112
|
+
|
|
113
|
+
return returncode, stdout_b, stderr_b
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def cmd_output(*cmd: str, **kwargs: Any) -> tuple[int, str, str | None]:
|
|
117
|
+
returncode, stdout_b, stderr_b = cmd_output_b(*cmd, **kwargs)
|
|
118
|
+
stdout = stdout_b.decode() if stdout_b is not None else None
|
|
119
|
+
stderr = stderr_b.decode() if stderr_b is not None else None
|
|
120
|
+
return returncode, stdout, stderr
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if sys.platform != 'win32': # pragma: win32 no cover
|
|
124
|
+
from os import openpty
|
|
125
|
+
import termios
|
|
126
|
+
|
|
127
|
+
class Pty:
|
|
128
|
+
def __init__(self) -> None:
|
|
129
|
+
self.r: int | None = None
|
|
130
|
+
self.w: int | None = None
|
|
131
|
+
|
|
132
|
+
def __enter__(self) -> Pty:
|
|
133
|
+
self.r, self.w = openpty()
|
|
134
|
+
|
|
135
|
+
# tty flags normally change \n to \r\n
|
|
136
|
+
attrs = termios.tcgetattr(self.w)
|
|
137
|
+
assert isinstance(attrs[1], int)
|
|
138
|
+
attrs[1] &= ~(termios.ONLCR | termios.OPOST)
|
|
139
|
+
termios.tcsetattr(self.w, termios.TCSANOW, attrs)
|
|
140
|
+
|
|
141
|
+
return self
|
|
142
|
+
|
|
143
|
+
def close_w(self) -> None:
|
|
144
|
+
if self.w is not None:
|
|
145
|
+
os.close(self.w)
|
|
146
|
+
self.w = None
|
|
147
|
+
|
|
148
|
+
def close_r(self) -> None:
|
|
149
|
+
assert self.r is not None
|
|
150
|
+
os.close(self.r)
|
|
151
|
+
self.r = None
|
|
152
|
+
|
|
153
|
+
def __exit__(
|
|
154
|
+
self,
|
|
155
|
+
exc_type: type[BaseException] | None,
|
|
156
|
+
exc_value: BaseException | None,
|
|
157
|
+
traceback: TracebackType | None,
|
|
158
|
+
) -> None:
|
|
159
|
+
self.close_w()
|
|
160
|
+
self.close_r()
|
|
161
|
+
|
|
162
|
+
def cmd_output_p(
|
|
163
|
+
*cmd: str,
|
|
164
|
+
check: bool = True,
|
|
165
|
+
**kwargs: Any,
|
|
166
|
+
) -> tuple[int, bytes, bytes | None]:
|
|
167
|
+
assert check is False
|
|
168
|
+
assert kwargs['stderr'] == subprocess.STDOUT, kwargs['stderr']
|
|
169
|
+
_setdefault_kwargs(kwargs)
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
cmd = parse_shebang.normalize_cmd(cmd)
|
|
173
|
+
except parse_shebang.ExecutableNotFoundError as e:
|
|
174
|
+
return e.to_output()
|
|
175
|
+
|
|
176
|
+
with open(os.devnull) as devnull, Pty() as pty:
|
|
177
|
+
assert pty.r is not None
|
|
178
|
+
kwargs.update({'stdin': devnull, 'stdout': pty.w, 'stderr': pty.w})
|
|
179
|
+
try:
|
|
180
|
+
proc = subprocess.Popen(cmd, **kwargs)
|
|
181
|
+
except OSError as e:
|
|
182
|
+
return _oserror_to_output(e)
|
|
183
|
+
|
|
184
|
+
pty.close_w()
|
|
185
|
+
|
|
186
|
+
buf = b''
|
|
187
|
+
while True:
|
|
188
|
+
try:
|
|
189
|
+
bts = os.read(pty.r, 4096)
|
|
190
|
+
except OSError as e:
|
|
191
|
+
if e.errno == errno.EIO:
|
|
192
|
+
bts = b''
|
|
193
|
+
else:
|
|
194
|
+
raise
|
|
195
|
+
else:
|
|
196
|
+
buf += bts
|
|
197
|
+
if not bts:
|
|
198
|
+
break
|
|
199
|
+
|
|
200
|
+
return proc.wait(), buf, None
|
|
201
|
+
else: # pragma: no cover
|
|
202
|
+
cmd_output_p = cmd_output_b
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _handle_readonly(
|
|
206
|
+
func: Callable[[str], object],
|
|
207
|
+
path: str,
|
|
208
|
+
exc: BaseException,
|
|
209
|
+
) -> None:
|
|
210
|
+
if (
|
|
211
|
+
func in (os.rmdir, os.remove, os.unlink) and
|
|
212
|
+
isinstance(exc, OSError) and
|
|
213
|
+
exc.errno in {errno.EACCES, errno.EPERM}
|
|
214
|
+
):
|
|
215
|
+
for p in (path, os.path.dirname(path)):
|
|
216
|
+
os.chmod(p, os.stat(p).st_mode | stat.S_IWUSR)
|
|
217
|
+
func(path)
|
|
218
|
+
else:
|
|
219
|
+
raise
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
if sys.version_info < (3, 12): # pragma: <3.12 cover
|
|
223
|
+
def _handle_readonly_old(
|
|
224
|
+
func: Callable[[str], object],
|
|
225
|
+
path: str,
|
|
226
|
+
excinfo: tuple[type[BaseException], BaseException, TracebackType],
|
|
227
|
+
) -> None:
|
|
228
|
+
return _handle_readonly(func, path, excinfo[1])
|
|
229
|
+
|
|
230
|
+
def rmtree(path: str) -> None:
|
|
231
|
+
shutil.rmtree(path, ignore_errors=False, onerror=_handle_readonly_old)
|
|
232
|
+
else: # pragma: >=3.12 cover
|
|
233
|
+
def rmtree(path: str) -> None:
|
|
234
|
+
"""On windows, rmtree fails for readonly dirs."""
|
|
235
|
+
shutil.rmtree(path, ignore_errors=False, onexc=_handle_readonly)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def win_exe(s: str) -> str:
|
|
239
|
+
return s if sys.platform != 'win32' else f'{s}.exe'
|
pre_commit/xargs.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import concurrent.futures
|
|
4
|
+
import contextlib
|
|
5
|
+
import math
|
|
6
|
+
import multiprocessing
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from collections.abc import Generator
|
|
12
|
+
from collections.abc import Iterable
|
|
13
|
+
from collections.abc import MutableMapping
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
from typing import Any
|
|
16
|
+
from typing import TypeVar
|
|
17
|
+
|
|
18
|
+
from pre_commit import parse_shebang
|
|
19
|
+
from pre_commit.util import cmd_output_b
|
|
20
|
+
from pre_commit.util import cmd_output_p
|
|
21
|
+
|
|
22
|
+
TArg = TypeVar('TArg')
|
|
23
|
+
TRet = TypeVar('TRet')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def cpu_count() -> int:
|
|
27
|
+
try:
|
|
28
|
+
# On systems that support it, this will return a more accurate count of
|
|
29
|
+
# usable CPUs for the current process, which will take into account
|
|
30
|
+
# cgroup limits
|
|
31
|
+
return len(os.sched_getaffinity(0))
|
|
32
|
+
except AttributeError:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
return multiprocessing.cpu_count()
|
|
37
|
+
except NotImplementedError:
|
|
38
|
+
return 1
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _environ_size(_env: MutableMapping[str, str] | None = None) -> int:
|
|
42
|
+
environ = _env if _env is not None else getattr(os, 'environb', os.environ)
|
|
43
|
+
size = 8 * len(environ) # number of pointers in `envp`
|
|
44
|
+
for k, v in environ.items():
|
|
45
|
+
size += len(k) + len(v) + 2 # c strings in `envp`
|
|
46
|
+
return size
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _get_platform_max_length() -> int: # pragma: no cover (platform specific)
|
|
50
|
+
if os.name == 'posix':
|
|
51
|
+
maximum = os.sysconf('SC_ARG_MAX') - 2048 - _environ_size()
|
|
52
|
+
maximum = max(min(maximum, 2 ** 17), 2 ** 12)
|
|
53
|
+
return maximum
|
|
54
|
+
elif os.name == 'nt':
|
|
55
|
+
return 2 ** 15 - 2048 # UNICODE_STRING max - headroom
|
|
56
|
+
else:
|
|
57
|
+
# posix minimum
|
|
58
|
+
return 2 ** 12
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _command_length(*cmd: str) -> int:
|
|
62
|
+
full_cmd = ' '.join(cmd)
|
|
63
|
+
|
|
64
|
+
# win32 uses the amount of characters, more details at:
|
|
65
|
+
# https://github.com/pre-commit/pre-commit/pull/839
|
|
66
|
+
if sys.platform == 'win32':
|
|
67
|
+
return len(full_cmd.encode('utf-16le')) // 2
|
|
68
|
+
else:
|
|
69
|
+
return len(full_cmd.encode(sys.getfilesystemencoding()))
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ArgumentTooLongError(RuntimeError):
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def partition(
|
|
77
|
+
cmd: Sequence[str],
|
|
78
|
+
varargs: Sequence[str],
|
|
79
|
+
target_concurrency: int,
|
|
80
|
+
_max_length: int | None = None,
|
|
81
|
+
) -> tuple[tuple[str, ...], ...]:
|
|
82
|
+
_max_length = _max_length or _get_platform_max_length()
|
|
83
|
+
|
|
84
|
+
# Generally, we try to partition evenly into at least `target_concurrency`
|
|
85
|
+
# partitions, but we don't want a bunch of tiny partitions.
|
|
86
|
+
max_args = max(4, math.ceil(len(varargs) / target_concurrency))
|
|
87
|
+
|
|
88
|
+
cmd = tuple(cmd)
|
|
89
|
+
ret = []
|
|
90
|
+
|
|
91
|
+
ret_cmd: list[str] = []
|
|
92
|
+
# Reversed so arguments are in order
|
|
93
|
+
varargs = list(reversed(varargs))
|
|
94
|
+
|
|
95
|
+
total_length = _command_length(*cmd) + 1
|
|
96
|
+
while varargs:
|
|
97
|
+
arg = varargs.pop()
|
|
98
|
+
|
|
99
|
+
arg_length = _command_length(arg) + 1
|
|
100
|
+
if (
|
|
101
|
+
total_length + arg_length <= _max_length and
|
|
102
|
+
len(ret_cmd) < max_args
|
|
103
|
+
):
|
|
104
|
+
ret_cmd.append(arg)
|
|
105
|
+
total_length += arg_length
|
|
106
|
+
elif not ret_cmd:
|
|
107
|
+
raise ArgumentTooLongError(arg)
|
|
108
|
+
else:
|
|
109
|
+
# We've exceeded the length, yield a command
|
|
110
|
+
ret.append(cmd + tuple(ret_cmd))
|
|
111
|
+
ret_cmd = []
|
|
112
|
+
total_length = _command_length(*cmd) + 1
|
|
113
|
+
varargs.append(arg)
|
|
114
|
+
|
|
115
|
+
ret.append(cmd + tuple(ret_cmd))
|
|
116
|
+
|
|
117
|
+
return tuple(ret)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@contextlib.contextmanager
|
|
121
|
+
def _thread_mapper(maxsize: int) -> Generator[
|
|
122
|
+
Callable[[Callable[[TArg], TRet], Iterable[TArg]], Iterable[TRet]],
|
|
123
|
+
]:
|
|
124
|
+
if maxsize == 1:
|
|
125
|
+
yield map
|
|
126
|
+
else:
|
|
127
|
+
with concurrent.futures.ThreadPoolExecutor(maxsize) as ex:
|
|
128
|
+
yield ex.map
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def xargs(
|
|
132
|
+
cmd: tuple[str, ...],
|
|
133
|
+
varargs: Sequence[str],
|
|
134
|
+
*,
|
|
135
|
+
color: bool = False,
|
|
136
|
+
target_concurrency: int = 1,
|
|
137
|
+
_max_length: int = _get_platform_max_length(),
|
|
138
|
+
**kwargs: Any,
|
|
139
|
+
) -> tuple[int, bytes]:
|
|
140
|
+
"""A simplified implementation of xargs.
|
|
141
|
+
|
|
142
|
+
color: Make a pty if on a platform that supports it
|
|
143
|
+
target_concurrency: Target number of partitions to run concurrently
|
|
144
|
+
"""
|
|
145
|
+
cmd_fn = cmd_output_p if color else cmd_output_b
|
|
146
|
+
retcode = 0
|
|
147
|
+
stdout = b''
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
cmd = parse_shebang.normalize_cmd(cmd)
|
|
151
|
+
except parse_shebang.ExecutableNotFoundError as e:
|
|
152
|
+
return e.to_output()[:2]
|
|
153
|
+
|
|
154
|
+
# on windows, batch files have a separate length limit than windows itself
|
|
155
|
+
if (
|
|
156
|
+
sys.platform == 'win32' and
|
|
157
|
+
cmd[0].lower().endswith(('.bat', '.cmd'))
|
|
158
|
+
): # pragma: win32 cover
|
|
159
|
+
# this is implementation details but the command gets translated into
|
|
160
|
+
# full/path/to/cmd.exe /c *cmd
|
|
161
|
+
cmd_exe = parse_shebang.find_executable('cmd.exe')
|
|
162
|
+
# 1024 is additionally subtracted to give headroom for further
|
|
163
|
+
# expansion inside the batch file
|
|
164
|
+
_max_length = 8192 - len(cmd_exe) - len(' /c ') - 1024
|
|
165
|
+
|
|
166
|
+
partitions = partition(cmd, varargs, target_concurrency, _max_length)
|
|
167
|
+
|
|
168
|
+
def run_cmd_partition(
|
|
169
|
+
run_cmd: tuple[str, ...],
|
|
170
|
+
) -> tuple[int, bytes, bytes | None]:
|
|
171
|
+
return cmd_fn(
|
|
172
|
+
*run_cmd, check=False, stderr=subprocess.STDOUT, **kwargs,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
threads = min(len(partitions), target_concurrency)
|
|
176
|
+
with _thread_mapper(threads) as thread_map:
|
|
177
|
+
results = thread_map(run_cmd_partition, partitions)
|
|
178
|
+
|
|
179
|
+
for proc_retcode, proc_out, _ in results:
|
|
180
|
+
if abs(proc_retcode) > abs(retcode):
|
|
181
|
+
retcode = proc_retcode
|
|
182
|
+
stdout += proc_out
|
|
183
|
+
|
|
184
|
+
return retcode, stdout
|
pre_commit/yaml.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
Loader = getattr(yaml, 'CSafeLoader', yaml.SafeLoader)
|
|
9
|
+
yaml_compose = functools.partial(yaml.compose, Loader=Loader)
|
|
10
|
+
yaml_load = functools.partial(yaml.load, Loader=Loader)
|
|
11
|
+
Dumper = getattr(yaml, 'CSafeDumper', yaml.SafeDumper)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def yaml_dump(o: Any, **kwargs: Any) -> str:
|
|
15
|
+
# when python/mypy#1484 is solved, this can be `functools.partial`
|
|
16
|
+
return yaml.dump(
|
|
17
|
+
o, Dumper=Dumper, default_flow_style=False, indent=4, sort_keys=False,
|
|
18
|
+
**kwargs,
|
|
19
|
+
)
|