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/color.py ADDED
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import os
5
+ import sys
6
+
7
+ if sys.platform == 'win32': # pragma: no cover (windows)
8
+ def _enable() -> None:
9
+ from ctypes import POINTER
10
+ from ctypes import windll
11
+ from ctypes import WinError
12
+ from ctypes import WINFUNCTYPE
13
+ from ctypes.wintypes import BOOL
14
+ from ctypes.wintypes import DWORD
15
+ from ctypes.wintypes import HANDLE
16
+
17
+ STD_ERROR_HANDLE = -12
18
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
19
+
20
+ def bool_errcheck(result, func, args):
21
+ if not result:
22
+ raise WinError()
23
+ return args
24
+
25
+ GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)(
26
+ ('GetStdHandle', windll.kernel32), ((1, 'nStdHandle'),),
27
+ )
28
+
29
+ GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))(
30
+ ('GetConsoleMode', windll.kernel32),
31
+ ((1, 'hConsoleHandle'), (2, 'lpMode')),
32
+ )
33
+ GetConsoleMode.errcheck = bool_errcheck
34
+
35
+ SetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, DWORD)(
36
+ ('SetConsoleMode', windll.kernel32),
37
+ ((1, 'hConsoleHandle'), (1, 'dwMode')),
38
+ )
39
+ SetConsoleMode.errcheck = bool_errcheck
40
+
41
+ # As of Windows 10, the Windows console supports (some) ANSI escape
42
+ # sequences, but it needs to be enabled using `SetConsoleMode` first.
43
+ #
44
+ # More info on the escape sequences supported:
45
+ # https://msdn.microsoft.com/en-us/library/windows/desktop/mt638032(v=vs.85).aspx
46
+ stderr = GetStdHandle(STD_ERROR_HANDLE)
47
+ flags = GetConsoleMode(stderr)
48
+ SetConsoleMode(stderr, flags | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
49
+
50
+ try:
51
+ _enable()
52
+ except OSError:
53
+ terminal_supports_color = False
54
+ else:
55
+ terminal_supports_color = True
56
+ else: # pragma: win32 no cover
57
+ terminal_supports_color = True
58
+
59
+ RED = '\033[41m'
60
+ GREEN = '\033[42m'
61
+ YELLOW = '\033[43;30m'
62
+ TURQUOISE = '\033[46;30m'
63
+ SUBTLE = '\033[2m'
64
+ NORMAL = '\033[m'
65
+
66
+
67
+ def format_color(text: str, color: str, use_color_setting: bool) -> str:
68
+ """Format text with color.
69
+
70
+ Args:
71
+ text - Text to be formatted with color if `use_color`
72
+ color - The color start string
73
+ use_color_setting - Whether or not to color
74
+ """
75
+ if use_color_setting:
76
+ return f'{color}{text}{NORMAL}'
77
+ else:
78
+ return text
79
+
80
+
81
+ COLOR_CHOICES = ('auto', 'always', 'never')
82
+
83
+
84
+ def use_color(setting: str) -> bool:
85
+ """Choose whether to use color based on the command argument.
86
+
87
+ Args:
88
+ setting - Either `auto`, `always`, or `never`
89
+ """
90
+ if setting not in COLOR_CHOICES:
91
+ raise ValueError(setting)
92
+
93
+ return (
94
+ setting == 'always' or (
95
+ setting == 'auto' and
96
+ sys.stderr.isatty() and
97
+ terminal_supports_color and
98
+ os.getenv('TERM') != 'dumb'
99
+ )
100
+ )
101
+
102
+
103
+ def add_color_option(parser: argparse.ArgumentParser) -> None:
104
+ parser.add_argument(
105
+ '--color', default=os.environ.get('PRE_COMMIT_COLOR', 'auto'),
106
+ type=use_color,
107
+ metavar='{' + ','.join(COLOR_CHOICES) + '}',
108
+ help='Whether to use color in output. Defaults to `%(default)s`.',
109
+ )
File without changes
@@ -0,0 +1,215 @@
1
+ from __future__ import annotations
2
+
3
+ import concurrent.futures
4
+ import os.path
5
+ import re
6
+ import tempfile
7
+ from collections.abc import Sequence
8
+ from typing import Any
9
+ from typing import NamedTuple
10
+
11
+ import pre_commit.constants as C
12
+ from pre_commit import git
13
+ from pre_commit import output
14
+ from pre_commit import xargs
15
+ from pre_commit.clientlib import InvalidManifestError
16
+ from pre_commit.clientlib import load_config
17
+ from pre_commit.clientlib import load_manifest
18
+ from pre_commit.clientlib import LOCAL
19
+ from pre_commit.clientlib import META
20
+ from pre_commit.commands.migrate_config import migrate_config
21
+ from pre_commit.util import CalledProcessError
22
+ from pre_commit.util import cmd_output
23
+ from pre_commit.util import cmd_output_b
24
+ from pre_commit.yaml import yaml_dump
25
+ from pre_commit.yaml import yaml_load
26
+
27
+
28
+ class RevInfo(NamedTuple):
29
+ repo: str
30
+ rev: str
31
+ frozen: str | None = None
32
+ hook_ids: frozenset[str] = frozenset()
33
+
34
+ @classmethod
35
+ def from_config(cls, config: dict[str, Any]) -> RevInfo:
36
+ return cls(config['repo'], config['rev'])
37
+
38
+ def update(self, tags_only: bool, freeze: bool) -> RevInfo:
39
+ with tempfile.TemporaryDirectory() as tmp:
40
+ _git = ('git', *git.NO_FS_MONITOR, '-C', tmp)
41
+
42
+ if tags_only:
43
+ tag_opt = '--abbrev=0'
44
+ else:
45
+ tag_opt = '--exact'
46
+ tag_cmd = (*_git, 'describe', 'FETCH_HEAD', '--tags', tag_opt)
47
+
48
+ git.init_repo(tmp, self.repo)
49
+ cmd_output_b(*_git, 'config', 'extensions.partialClone', 'true')
50
+ cmd_output_b(
51
+ *_git, 'fetch', 'origin', 'HEAD',
52
+ '--quiet', '--filter=blob:none', '--tags',
53
+ )
54
+
55
+ try:
56
+ rev = cmd_output(*tag_cmd)[1].strip()
57
+ except CalledProcessError:
58
+ rev = cmd_output(*_git, 'rev-parse', 'FETCH_HEAD')[1].strip()
59
+ else:
60
+ if tags_only:
61
+ rev = git.get_best_candidate_tag(rev, tmp)
62
+
63
+ frozen = None
64
+ if freeze:
65
+ exact = cmd_output(*_git, 'rev-parse', rev)[1].strip()
66
+ if exact != rev:
67
+ rev, frozen = exact, rev
68
+
69
+ try:
70
+ # workaround for windows -- see #2865
71
+ cmd_output_b(*_git, 'show', f'{rev}:{C.MANIFEST_FILE}')
72
+ cmd_output(*_git, 'checkout', rev, '--', C.MANIFEST_FILE)
73
+ except CalledProcessError:
74
+ pass # this will be caught by manifest validating code
75
+ try:
76
+ manifest = load_manifest(os.path.join(tmp, C.MANIFEST_FILE))
77
+ except InvalidManifestError as e:
78
+ raise RepositoryCannotBeUpdatedError(f'[{self.repo}] {e}')
79
+ else:
80
+ hook_ids = frozenset(hook['id'] for hook in manifest)
81
+
82
+ return self._replace(rev=rev, frozen=frozen, hook_ids=hook_ids)
83
+
84
+
85
+ class RepositoryCannotBeUpdatedError(RuntimeError):
86
+ pass
87
+
88
+
89
+ def _check_hooks_still_exist_at_rev(
90
+ repo_config: dict[str, Any],
91
+ info: RevInfo,
92
+ ) -> None:
93
+ # See if any of our hooks were deleted with the new commits
94
+ hooks = {hook['id'] for hook in repo_config['hooks']}
95
+ hooks_missing = hooks - info.hook_ids
96
+ if hooks_missing:
97
+ raise RepositoryCannotBeUpdatedError(
98
+ f'[{info.repo}] Cannot update because the update target is '
99
+ f'missing these hooks: {", ".join(sorted(hooks_missing))}',
100
+ )
101
+
102
+
103
+ def _update_one(
104
+ i: int,
105
+ repo: dict[str, Any],
106
+ *,
107
+ tags_only: bool,
108
+ freeze: bool,
109
+ ) -> tuple[int, RevInfo, RevInfo]:
110
+ old = RevInfo.from_config(repo)
111
+ new = old.update(tags_only=tags_only, freeze=freeze)
112
+ _check_hooks_still_exist_at_rev(repo, new)
113
+ return i, old, new
114
+
115
+
116
+ REV_LINE_RE = re.compile(r'^(\s+)rev:(\s*)([\'"]?)([^\s#]+)(.*)(\r?\n)$')
117
+
118
+
119
+ def _original_lines(
120
+ path: str,
121
+ rev_infos: list[RevInfo | None],
122
+ retry: bool = False,
123
+ ) -> tuple[list[str], list[int]]:
124
+ """detect `rev:` lines or reformat the file"""
125
+ with open(path, newline='') as f:
126
+ original = f.read()
127
+
128
+ lines = original.splitlines(True)
129
+ idxs = [i for i, line in enumerate(lines) if REV_LINE_RE.match(line)]
130
+ if len(idxs) == len(rev_infos):
131
+ return lines, idxs
132
+ elif retry:
133
+ raise AssertionError('could not find rev lines')
134
+ else:
135
+ with open(path, 'w') as f:
136
+ f.write(yaml_dump(yaml_load(original)))
137
+ return _original_lines(path, rev_infos, retry=True)
138
+
139
+
140
+ def _write_new_config(path: str, rev_infos: list[RevInfo | None]) -> None:
141
+ lines, idxs = _original_lines(path, rev_infos)
142
+
143
+ for idx, rev_info in zip(idxs, rev_infos):
144
+ if rev_info is None:
145
+ continue
146
+ match = REV_LINE_RE.match(lines[idx])
147
+ assert match is not None
148
+ new_rev_s = yaml_dump({'rev': rev_info.rev}, default_style=match[3])
149
+ new_rev = new_rev_s.split(':', 1)[1].strip()
150
+ if rev_info.frozen is not None:
151
+ comment = f' # frozen: {rev_info.frozen}'
152
+ elif match[5].strip().startswith('# frozen:'):
153
+ comment = ''
154
+ else:
155
+ comment = match[5]
156
+ lines[idx] = f'{match[1]}rev:{match[2]}{new_rev}{comment}{match[6]}'
157
+
158
+ with open(path, 'w', newline='') as f:
159
+ f.write(''.join(lines))
160
+
161
+
162
+ def autoupdate(
163
+ config_file: str,
164
+ tags_only: bool,
165
+ freeze: bool,
166
+ repos: Sequence[str] = (),
167
+ jobs: int = 1,
168
+ ) -> int:
169
+ """Auto-update the pre-commit config to the latest versions of repos."""
170
+ migrate_config(config_file, quiet=True)
171
+ changed = False
172
+ retv = 0
173
+
174
+ config_repos = [
175
+ repo for repo in load_config(config_file)['repos']
176
+ if repo['repo'] not in {LOCAL, META}
177
+ ]
178
+
179
+ rev_infos: list[RevInfo | None] = [None] * len(config_repos)
180
+ jobs = jobs or xargs.cpu_count() # 0 => number of cpus
181
+ jobs = min(jobs, len(repos) or len(config_repos)) # max 1-per-thread
182
+ jobs = max(jobs, 1) # at least one thread
183
+ with concurrent.futures.ThreadPoolExecutor(jobs) as exe:
184
+ futures = [
185
+ exe.submit(
186
+ _update_one,
187
+ i, repo, tags_only=tags_only, freeze=freeze,
188
+ )
189
+ for i, repo in enumerate(config_repos)
190
+ if not repos or repo['repo'] in repos
191
+ ]
192
+ for future in concurrent.futures.as_completed(futures):
193
+ try:
194
+ i, old, new = future.result()
195
+ except RepositoryCannotBeUpdatedError as e:
196
+ output.write_line(str(e))
197
+ retv = 1
198
+ else:
199
+ if new.rev != old.rev:
200
+ changed = True
201
+ if new.frozen:
202
+ new_s = f'{new.frozen} (frozen)'
203
+ else:
204
+ new_s = new.rev
205
+ msg = f'updating {old.rev} -> {new_s}'
206
+ rev_infos[i] = new
207
+ else:
208
+ msg = 'already up to date!'
209
+
210
+ output.write_line(f'[{old.repo}] {msg}')
211
+
212
+ if changed:
213
+ _write_new_config(config_file, rev_infos)
214
+
215
+ return retv
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ import os.path
4
+
5
+ from pre_commit import output
6
+ from pre_commit.store import Store
7
+ from pre_commit.util import rmtree
8
+
9
+
10
+ def clean(store: Store) -> int:
11
+ legacy_path = os.path.expanduser('~/.pre-commit')
12
+ for directory in (store.directory, legacy_path):
13
+ if os.path.exists(directory):
14
+ rmtree(directory)
15
+ output.write_line(f'Cleaned {directory}.')
16
+ return 0
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ import os.path
4
+ from typing import Any
5
+
6
+ import pre_commit.constants as C
7
+ from pre_commit import output
8
+ from pre_commit.clientlib import InvalidConfigError
9
+ from pre_commit.clientlib import InvalidManifestError
10
+ from pre_commit.clientlib import load_config
11
+ from pre_commit.clientlib import load_manifest
12
+ from pre_commit.clientlib import LOCAL
13
+ from pre_commit.clientlib import META
14
+ from pre_commit.store import Store
15
+ from pre_commit.util import rmtree
16
+
17
+
18
+ def _mark_used_repos(
19
+ store: Store,
20
+ all_repos: dict[tuple[str, str], str],
21
+ unused_repos: set[tuple[str, str]],
22
+ repo: dict[str, Any],
23
+ ) -> None:
24
+ if repo['repo'] == META:
25
+ return
26
+ elif repo['repo'] == LOCAL:
27
+ for hook in repo['hooks']:
28
+ deps = hook.get('additional_dependencies')
29
+ unused_repos.discard((
30
+ store.db_repo_name(repo['repo'], deps),
31
+ C.LOCAL_REPO_VERSION,
32
+ ))
33
+ else:
34
+ key = (repo['repo'], repo['rev'])
35
+ path = all_repos.get(key)
36
+ # can't inspect manifest if it isn't cloned
37
+ if path is None:
38
+ return
39
+
40
+ try:
41
+ manifest = load_manifest(os.path.join(path, C.MANIFEST_FILE))
42
+ except InvalidManifestError:
43
+ return
44
+ else:
45
+ unused_repos.discard(key)
46
+ by_id = {hook['id']: hook for hook in manifest}
47
+
48
+ for hook in repo['hooks']:
49
+ if hook['id'] not in by_id:
50
+ continue
51
+
52
+ deps = hook.get(
53
+ 'additional_dependencies',
54
+ by_id[hook['id']]['additional_dependencies'],
55
+ )
56
+ unused_repos.discard((
57
+ store.db_repo_name(repo['repo'], deps), repo['rev'],
58
+ ))
59
+
60
+
61
+ def _gc(store: Store) -> int:
62
+ with store.exclusive_lock(), store.connect() as db:
63
+ store._create_configs_table(db)
64
+
65
+ repos = db.execute('SELECT repo, ref, path FROM repos').fetchall()
66
+ all_repos = {(repo, ref): path for repo, ref, path in repos}
67
+ unused_repos = set(all_repos)
68
+
69
+ configs_rows = db.execute('SELECT path FROM configs').fetchall()
70
+ configs = [path for path, in configs_rows]
71
+
72
+ dead_configs = []
73
+ for config_path in configs:
74
+ try:
75
+ config = load_config(config_path)
76
+ except InvalidConfigError:
77
+ dead_configs.append(config_path)
78
+ continue
79
+ else:
80
+ for repo in config['repos']:
81
+ _mark_used_repos(store, all_repos, unused_repos, repo)
82
+
83
+ paths = [(path,) for path in dead_configs]
84
+ db.executemany('DELETE FROM configs WHERE path = ?', paths)
85
+
86
+ db.executemany(
87
+ 'DELETE FROM repos WHERE repo = ? and ref = ?',
88
+ sorted(unused_repos),
89
+ )
90
+ for k in unused_repos:
91
+ rmtree(all_repos[k])
92
+
93
+ return len(unused_repos)
94
+
95
+
96
+ def gc(store: Store) -> int:
97
+ output.write_line(f'{_gc(store)} repo(s) removed.')
98
+ return 0
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import subprocess
5
+ from collections.abc import Sequence
6
+
7
+ from pre_commit.parse_shebang import normalize_cmd
8
+
9
+
10
+ def add_parsers(parser: argparse.ArgumentParser) -> None:
11
+ subparsers = parser.add_subparsers(dest='tool')
12
+
13
+ cd_parser = subparsers.add_parser(
14
+ 'cd', help='cd to a subdir and run the command',
15
+ )
16
+ cd_parser.add_argument('subdir')
17
+ cd_parser.add_argument('cmd', nargs=argparse.REMAINDER)
18
+
19
+ ignore_exit_code_parser = subparsers.add_parser(
20
+ 'ignore-exit-code', help='run the command but ignore the exit code',
21
+ )
22
+ ignore_exit_code_parser.add_argument('cmd', nargs=argparse.REMAINDER)
23
+
24
+ n1_parser = subparsers.add_parser(
25
+ 'n1', help='run the command once per filename',
26
+ )
27
+ n1_parser.add_argument('cmd', nargs=argparse.REMAINDER)
28
+
29
+
30
+ def _cmd_filenames(cmd: tuple[str, ...]) -> tuple[
31
+ tuple[str, ...],
32
+ tuple[str, ...],
33
+ ]:
34
+ for idx, val in enumerate(reversed(cmd)):
35
+ if val == '--':
36
+ split = len(cmd) - idx
37
+ break
38
+ else:
39
+ raise SystemExit('hazmat entry must end with `--`')
40
+
41
+ return cmd[:split - 1], cmd[split:]
42
+
43
+
44
+ def cd(subdir: str, cmd: tuple[str, ...]) -> int:
45
+ cmd, filenames = _cmd_filenames(cmd)
46
+
47
+ prefix = f'{subdir}/'
48
+ new_filenames = []
49
+ for filename in filenames:
50
+ if not filename.startswith(prefix):
51
+ raise SystemExit(f'unexpected file without {prefix=}: {filename}')
52
+ else:
53
+ new_filenames.append(filename.removeprefix(prefix))
54
+
55
+ cmd = normalize_cmd(cmd)
56
+ return subprocess.call((*cmd, *new_filenames), cwd=subdir)
57
+
58
+
59
+ def ignore_exit_code(cmd: tuple[str, ...]) -> int:
60
+ cmd = normalize_cmd(cmd)
61
+ subprocess.call(cmd)
62
+ return 0
63
+
64
+
65
+ def n1(cmd: tuple[str, ...]) -> int:
66
+ cmd, filenames = _cmd_filenames(cmd)
67
+ cmd = normalize_cmd(cmd)
68
+ ret = 0
69
+ for filename in filenames:
70
+ ret |= subprocess.call((*cmd, filename))
71
+ return ret
72
+
73
+
74
+ def impl(args: argparse.Namespace) -> int:
75
+ args.cmd = tuple(args.cmd)
76
+ if args.tool == 'cd':
77
+ return cd(args.subdir, args.cmd)
78
+ elif args.tool == 'ignore-exit-code':
79
+ return ignore_exit_code(args.cmd)
80
+ elif args.tool == 'n1':
81
+ return n1(args.cmd)
82
+ else:
83
+ raise NotImplementedError(f'unexpected tool: {args.tool}')
84
+
85
+
86
+ def main(argv: Sequence[str] | None = None) -> int:
87
+ parser = argparse.ArgumentParser()
88
+ add_parsers(parser)
89
+ args = parser.parse_args(argv)
90
+
91
+ return impl(args)
92
+
93
+
94
+ if __name__ == '__main__':
95
+ raise SystemExit(main())