Skip to content

Commit cc45b5e

Browse files
committed
Improve git hook shebang creation
1 parent 78f406a commit cc45b5e

File tree

2 files changed

+43
-17
lines changed

2 files changed

+43
-17
lines changed

pre_commit/commands/install_uninstall.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
CURRENT_HASH = '138fd403232d2ddd5efb44317e38bf03'
3131
TEMPLATE_START = '# start templated\n'
3232
TEMPLATE_END = '# end templated\n'
33+
# Homebrew/homebrew-core#35825: be more timid about appropriate `PATH`
34+
# #1312 os.defpath is too restrictive on BSD
35+
POSIX_SEARCH_PATH = ('/usr/local/bin', '/usr/bin', '/bin')
36+
SYS_EXE = os.path.basename(os.path.realpath(sys.executable))
3337

3438

3539
def _hook_paths(
@@ -51,20 +55,21 @@ def is_our_script(filename: str) -> bool:
5155

5256
def shebang() -> str:
5357
if sys.platform == 'win32':
54-
py = 'python'
58+
py = SYS_EXE
5559
else:
56-
# Homebrew/homebrew-core#35825: be more timid about appropriate `PATH`
57-
path_choices = [p for p in os.defpath.split(os.pathsep) if p]
5860
exe_choices = [
5961
f'python{sys.version_info[0]}.{sys.version_info[1]}',
6062
f'python{sys.version_info[0]}',
6163
]
62-
for path, exe in itertools.product(path_choices, exe_choices):
64+
# avoid searching for bare `python` as it's likely to be python 2
65+
if SYS_EXE != 'python':
66+
exe_choices.append(SYS_EXE)
67+
for path, exe in itertools.product(POSIX_SEARCH_PATH, exe_choices):
6368
if os.access(os.path.join(path, exe), os.X_OK):
6469
py = exe
6570
break
6671
else:
67-
py = 'python'
72+
py = SYS_EXE
6873
return f'#!/usr/bin/env {py}'
6974

7075

tests/commands/install_uninstall_test.py

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from unittest import mock
55

66
import pre_commit.constants as C
7+
from pre_commit.commands import install_uninstall
78
from pre_commit.commands.install_uninstall import CURRENT_HASH
89
from pre_commit.commands.install_uninstall import install
910
from pre_commit.commands.install_uninstall import install_hooks
@@ -39,25 +40,36 @@ def test_is_previous_pre_commit(tmpdir):
3940
assert is_our_script(f.strpath)
4041

4142

43+
def patch_platform(platform):
44+
return mock.patch.object(sys, 'platform', platform)
45+
46+
47+
def patch_lookup_path(path):
48+
return mock.patch.object(install_uninstall, 'POSIX_SEARCH_PATH', path)
49+
50+
51+
def patch_sys_exe(exe):
52+
return mock.patch.object(install_uninstall, 'SYS_EXE', exe)
53+
54+
4255
def test_shebang_windows():
43-
with mock.patch.object(sys, 'platform', 'win32'):
44-
assert shebang() == '#!/usr/bin/env python'
56+
with patch_platform('win32'), patch_sys_exe('python.exe'):
57+
assert shebang() == '#!/usr/bin/env python.exe'
4558

4659

4760
def test_shebang_posix_not_on_path():
48-
with mock.patch.object(sys, 'platform', 'posix'):
49-
with mock.patch.object(os, 'defpath', ''):
50-
assert shebang() == '#!/usr/bin/env python'
61+
with patch_platform('posix'), patch_lookup_path(()):
62+
with patch_sys_exe('python3.6'):
63+
assert shebang() == '#!/usr/bin/env python3.6'
5164

5265

5366
def test_shebang_posix_on_path(tmpdir):
5467
exe = tmpdir.join(f'python{sys.version_info[0]}').ensure()
5568
make_executable(exe)
5669

57-
with mock.patch.object(sys, 'platform', 'posix'):
58-
with mock.patch.object(os, 'defpath', tmpdir.strpath):
59-
expected = f'#!/usr/bin/env python{sys.version_info[0]}'
60-
assert shebang() == expected
70+
with patch_platform('posix'), patch_lookup_path((tmpdir.strpath,)):
71+
with patch_sys_exe('python'):
72+
assert shebang() == f'#!/usr/bin/env python{sys.version_info[0]}'
6173

6274

6375
def test_install_pre_commit(in_git_dir, store):
@@ -250,9 +262,18 @@ def _path_without_us():
250262
def test_environment_not_sourced(tempdir_factory, store):
251263
path = make_consuming_repo(tempdir_factory, 'script_hooks_repo')
252264
with cwd(path):
253-
# Patch the executable to simulate rming virtualenv
254-
with mock.patch.object(sys, 'executable', '/does-not-exist'):
255-
assert not install(C.CONFIG_FILE, store, hook_types=['pre-commit'])
265+
assert not install(C.CONFIG_FILE, store, hook_types=['pre-commit'])
266+
# simulate deleting the virtualenv by rewriting the exe
267+
hook = os.path.join(path, '.git/hooks/pre-commit')
268+
with open(hook) as f:
269+
src = f.read()
270+
src = re.sub(
271+
'\nINSTALL_PYTHON =.*\n',
272+
'\nINSTALL_PYTHON = "/dne"\n',
273+
src,
274+
)
275+
with open(hook, 'w') as f:
276+
f.write(src)
256277

257278
# Use a specific homedir to ignore --user installs
258279
homedir = tempdir_factory.get()

0 commit comments

Comments
 (0)