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
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
+ )