pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/pre-commit/pre-commit/commit/3358a3b54077cd8c8c99618aed003b771a21cb65

origen="anonymous" media="all" rel="stylesheet" href="https://github.githubassets.com/assets/global-9c8f61f9f58ad7b2.css" /> Merge pull request #3585 from pre-commit/hazmat · pre-commit/pre-commit@3358a3b · GitHub
Skip to content

Commit 3358a3b

Browse files
authored
Merge pull request #3585 from pre-commit/hazmat
add pre-commit hazmat
2 parents e436690 + bdf6879 commit 3358a3b

7 files changed

Lines changed: 243 additions & 2 deletions

File tree

pre_commit/commands/hazmat.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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())

pre_commit/lang_base.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import random
66
import re
77
import shlex
8+
import sys
89
from collections.abc import Generator
910
from collections.abc import Sequence
1011
from typing import Any
@@ -171,7 +172,10 @@ def run_xargs(
171172

172173

173174
def hook_cmd(entry: str, args: Sequence[str]) -> tuple[str, ...]:
174-
return (*shlex.split(entry), *args)
175+
cmd = shlex.split(entry)
176+
if cmd[:2] == ['pre-commit', 'hazmat']:
177+
cmd = [sys.executable, '-m', 'pre_commit.commands.hazmat', *cmd[2:]]
178+
return (*cmd, *args)
175179

176180

177181
def basic_run_hook(

pre_commit/main.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from pre_commit import clientlib
1111
from pre_commit import git
1212
from pre_commit.color import add_color_option
13+
from pre_commit.commands import hazmat
1314
from pre_commit.commands.autoupdate import autoupdate
1415
from pre_commit.commands.clean import clean
1516
from pre_commit.commands.gc import gc
@@ -41,7 +42,7 @@
4142
os.environ.pop('PYTHONEXECUTABLE', None)
4243

4344
COMMANDS_NO_GIT = {
44-
'clean', 'gc', 'init-templatedir', 'sample-config',
45+
'clean', 'gc', 'hazmat', 'init-templatedir', 'sample-config',
4546
'validate-config', 'validate-manifest',
4647
}
4748

@@ -245,6 +246,11 @@ def _add_cmd(name: str, *, help: str) -> argparse.ArgumentParser:
245246

246247
_add_cmd('gc', help='Clean unused cached repos.')
247248

249+
hazmat_parser = _add_cmd(
250+
'hazmat', help='Composable tools for rare use in hook `entry`.',
251+
)
252+
hazmat.add_parsers(hazmat_parser)
253+
248254
init_templatedir_parser = _add_cmd(
249255
'init-templatedir',
250256
help=(
@@ -389,6 +395,8 @@ def _add_cmd(name: str, *, help: str) -> argparse.ArgumentParser:
389395
return clean(store)
390396
elif args.command == 'gc':
391397
return gc(store)
398+
elif args.command == 'hazmat':
399+
return hazmat.impl(args)
392400
elif args.command == 'hook-impl':
393401
return hook_impl(
394402
store,

tests/commands/hazmat_test.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
from __future__ import annotations
2+
3+
import sys
4+
5+
import pytest
6+
7+
from pre_commit.commands.hazmat import _cmd_filenames
8+
from pre_commit.commands.hazmat import main
9+
from testing.util import cwd
10+
11+
12+
def test_cmd_filenames_no_dash_dash():
13+
with pytest.raises(SystemExit) as excinfo:
14+
_cmd_filenames(('no', 'dashdash', 'here'))
15+
msg, = excinfo.value.args
16+
assert msg == 'hazmat entry must end with `--`'
17+
18+
19+
def test_cmd_filenames_no_filenames():
20+
cmd, filenames = _cmd_filenames(('hello', 'world', '--'))
21+
assert cmd == ('hello', 'world')
22+
assert filenames == ()
23+
24+
25+
def test_cmd_filenames_some_filenames():
26+
cmd, filenames = _cmd_filenames(('hello', 'world', '--', 'f1', 'f2'))
27+
assert cmd == ('hello', 'world')
28+
assert filenames == ('f1', 'f2')
29+
30+
31+
def test_cmd_filenames_multiple_dashdash():
32+
cmd, filenames = _cmd_filenames(('hello', '--', 'arg', '--', 'f1', 'f2'))
33+
assert cmd == ('hello', '--', 'arg')
34+
assert filenames == ('f1', 'f2')
35+
36+
37+
def test_cd_unexpected_filename():
38+
with pytest.raises(SystemExit) as excinfo:
39+
main(('cd', 'subdir', 'cmd', '--', 'subdir/1', 'not-subdir/2'))
40+
msg, = excinfo.value.args
41+
assert msg == "unexpected file without prefix='subdir/': not-subdir/2"
42+
43+
44+
def _norm(out):
45+
return out.replace('\r\n', '\n')
46+
47+
48+
def test_cd(tmp_path, capfd):
49+
subdir = tmp_path.joinpath('subdir')
50+
subdir.mkdir()
51+
subdir.joinpath('a').write_text('a')
52+
subdir.joinpath('b').write_text('b')
53+
54+
with cwd(tmp_path):
55+
ret = main((
56+
'cd', 'subdir',
57+
sys.executable, '-c',
58+
'import os; print(os.getcwd());'
59+
'import sys; [print(open(f).read()) for f in sys.argv[1:]]',
60+
'--',
61+
'subdir/a', 'subdir/b',
62+
))
63+
64+
assert ret == 0
65+
out, err = capfd.readouterr()
66+
assert _norm(out) == f'{subdir}\na\nb\n'
67+
assert err == ''
68+
69+
70+
def test_ignore_exit_code(capfd):
71+
ret = main((
72+
'ignore-exit-code', sys.executable, '-c', 'raise SystemExit("bye")',
73+
))
74+
assert ret == 0
75+
out, err = capfd.readouterr()
76+
assert out == ''
77+
assert _norm(err) == 'bye\n'
78+
79+
80+
def test_n1(capfd):
81+
ret = main((
82+
'n1', sys.executable, '-c', 'import sys; print(sys.argv[1:])',
83+
'--',
84+
'foo', 'bar', 'baz',
85+
))
86+
assert ret == 0
87+
out, err = capfd.readouterr()
88+
assert _norm(out) == "['foo']\n['bar']\n['baz']\n"
89+
assert err == ''
90+
91+
92+
def test_n1_some_error_code():
93+
ret = main((
94+
'n1', sys.executable, '-c',
95+
'import sys; raise SystemExit(sys.argv[1] == "error")',
96+
'--',
97+
'ok', 'error', 'ok',
98+
))
99+
assert ret == 1

tests/lang_base_test.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,15 @@ def test_basic_run_hook(tmp_path):
164164
assert ret == 0
165165
out = out.replace(b'\r\n', b'\n')
166166
assert out == b'hi hello file file file\n'
167+
168+
169+
def test_hook_cmd():
170+
assert lang_base.hook_cmd('echo hi', ()) == ('echo', 'hi')
171+
172+
173+
def test_hook_cmd_hazmat():
174+
ret = lang_base.hook_cmd('pre-commit hazmat cd a echo -- b', ())
175+
assert ret == (
176+
sys.executable, '-m', 'pre_commit.commands.hazmat',
177+
'cd', 'a', 'echo', '--', 'b',
178+
)

tests/main_test.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import pre_commit.constants as C
1111
from pre_commit import main
12+
from pre_commit.commands import hazmat
1213
from pre_commit.errors import FatalError
1314
from pre_commit.util import cmd_output
1415
from testing.auto_namedtuple import auto_namedtuple
@@ -157,6 +158,17 @@ def test_all_cmds(command, mock_commands, mock_store_dir):
157158
assert_only_one_mock_called(mock_commands)
158159

159160

161+
def test_hazmat(mock_store_dir):
162+
with mock.patch.object(hazmat, 'impl') as mck:
163+
main.main(('hazmat', 'cd', 'subdir', '--', 'cmd', '--', 'f1', 'f2'))
164+
assert mck.call_count == 1
165+
(arg,), dct = mck.call_args
166+
assert dct == {}
167+
assert arg.tool == 'cd'
168+
assert arg.subdir == 'subdir'
169+
assert arg.cmd == ['cmd', '--', 'f1', 'f2']
170+
171+
160172
def test_try_repo(mock_store_dir):
161173
with mock.patch.object(main, 'try_repo') as patch:
162174
main.main(('try-repo', '.'))

tests/repository_test.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,3 +506,14 @@ def test_args_with_spaces_and_quotes(tmp_path):
506506

507507
expected = b"['i have spaces', 'and\"\\'quotes', '$and !this']\n"
508508
assert ret == (0, expected)
509+
510+
511+
def test_hazmat(tmp_path):
512+
ret = run_language(
513+
tmp_path, unsupported,
514+
f'pre-commit hazmat ignore-exit-code {shlex.quote(sys.executable)} '
515+
f"-c 'import sys; raise SystemExit(sys.argv[1:])'",
516+
('f1', 'f2'),
517+
)
518+
expected = b"['f1', 'f2']\n"
519+
assert ret == (0, expected)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy