Add utility for parsing shebangs and resolving PATH

This commit is contained in:
Anthony Sottile 2016-03-21 21:08:44 -07:00
parent a932315a15
commit 82369fd99f
6 changed files with 267 additions and 13 deletions

View file

@ -15,12 +15,12 @@ from pre_commit.commands.install_uninstall import IDENTIFYING_HASH
from pre_commit.commands.install_uninstall import install
from pre_commit.commands.install_uninstall import is_our_pre_commit
from pre_commit.commands.install_uninstall import is_previous_pre_commit
from pre_commit.commands.install_uninstall import make_executable
from pre_commit.commands.install_uninstall import PREVIOUS_IDENTIFYING_HASHES
from pre_commit.commands.install_uninstall import uninstall
from pre_commit.runner import Runner
from pre_commit.util import cmd_output
from pre_commit.util import cwd
from pre_commit.util import make_executable
from pre_commit.util import mkdirp
from pre_commit.util import resource_filename
from testing.fixtures import git_dir
@ -473,6 +473,8 @@ def test_installed_from_venv(tempdir_factory):
'TERM': os.environ.get('TERM', ''),
# Windows needs this to import `random`
'SYSTEMROOT': os.environ.get('SYSTEMROOT', ''),
# Windows needs this to resolve executables
'PATHEXT': os.environ.get('PATHEXT', ''),
},
)
assert ret == 0

154
tests/parse_shebang_test.py Normal file
View file

@ -0,0 +1,154 @@
from __future__ import absolute_import
from __future__ import unicode_literals
import contextlib
import distutils.spawn
import io
import os
import sys
import pytest
from pre_commit import parse_shebang
from pre_commit.envcontext import envcontext
from pre_commit.envcontext import Var
from pre_commit.util import make_executable
@pytest.mark.parametrize(
('s', 'expected'),
(
(b'', ()),
(b'#!/usr/bin/python', ('/usr/bin/python',)),
(b'#!/usr/bin/env python', ('python',)),
(b'#! /usr/bin/python', ('/usr/bin/python',)),
(b'#!/usr/bin/foo python', ('/usr/bin/foo', 'python')),
(b'\xf9\x93\x01\x42\xcd', ()),
(b'#!\xf9\x93\x01\x42\xcd', ()),
(b'#!\x00\x00\x00\x00', ()),
),
)
def test_parse_bytesio(s, expected):
assert parse_shebang.parse_bytesio(io.BytesIO(s)) == expected
def test_file_doesnt_exist():
assert parse_shebang.parse_filename('herp derp derp') == ()
@pytest.mark.xfail(
sys.platform == 'win32', reason='Windows says everything is X_OK',
)
def test_file_not_executable(tmpdir):
x = tmpdir.join('f')
x.write_text('#!/usr/bin/env python', encoding='UTF-8')
assert parse_shebang.parse_filename(x.strpath) == ()
def test_simple_case(tmpdir):
x = tmpdir.join('f')
x.write_text('#!/usr/bin/env python', encoding='UTF-8')
make_executable(x.strpath)
assert parse_shebang.parse_filename(x.strpath) == ('python',)
def test_find_executable_full_path():
assert parse_shebang.find_executable(sys.executable) == sys.executable
def test_find_executable_on_path():
expected = distutils.spawn.find_executable('echo')
assert parse_shebang.find_executable('echo') == expected
def test_find_executable_not_found_none():
assert parse_shebang.find_executable('not-a-real-executable') is None
def write_executable(shebang, filename='run'):
os.mkdir('bin')
path = os.path.join('bin', filename)
with io.open(path, 'w') as f:
f.write('#!{0}'.format(shebang))
make_executable(path)
return path
@contextlib.contextmanager
def bin_on_path():
bindir = os.path.join(os.getcwd(), 'bin')
with envcontext((('PATH', (bindir, os.pathsep, Var('PATH'))),)):
yield
def test_find_executable_path_added(in_tmpdir):
path = os.path.abspath(write_executable('/usr/bin/env sh'))
assert parse_shebang.find_executable('run') is None
with bin_on_path():
assert parse_shebang.find_executable('run') == path
def test_find_executable_path_ext(in_tmpdir):
"""Windows exports PATHEXT as a list of extensions to automatically add
to executables when doing PATH searching.
"""
exe_path = os.path.abspath(write_executable(
'/usr/bin/env sh', filename='run.myext',
))
env_path = {'PATH': os.path.dirname(exe_path)}
env_path_ext = dict(env_path, PATHEXT=os.pathsep.join(('.exe', '.myext')))
assert parse_shebang.find_executable('run') is None
assert parse_shebang.find_executable('run', _environ=env_path) is None
ret = parse_shebang.find_executable('run.myext', _environ=env_path)
assert ret == exe_path
ret = parse_shebang.find_executable('run', _environ=env_path_ext)
assert ret == exe_path
def test_normexe_does_not_exist():
with pytest.raises(OSError) as excinfo:
parse_shebang.normexe('i-dont-exist-lol')
assert excinfo.value.args == ('Executable i-dont-exist-lol not found',)
def test_normexe_already_full_path():
assert parse_shebang.normexe(sys.executable) == sys.executable
def test_normexe_gives_full_path():
expected = distutils.spawn.find_executable('echo')
assert parse_shebang.normexe('echo') == expected
assert os.sep in expected
def test_normalize_cmd_trivial():
cmd = (distutils.spawn.find_executable('echo'), 'hi')
assert parse_shebang.normalize_cmd(cmd) == cmd
def test_normalize_cmd_PATH():
cmd = ('python', '--version')
expected = (distutils.spawn.find_executable('python'), '--version')
assert parse_shebang.normalize_cmd(cmd) == expected
def test_normalize_cmd_shebang(in_tmpdir):
python = distutils.spawn.find_executable('python')
path = write_executable(python.replace(os.sep, '/'))
assert parse_shebang.normalize_cmd((path,)) == (python, path)
def test_normalize_cmd_PATH_shebang_full_path(in_tmpdir):
python = distutils.spawn.find_executable('python')
path = write_executable(python.replace(os.sep, '/'))
with bin_on_path():
ret = parse_shebang.normalize_cmd(('run',))
assert ret == (python, os.path.abspath(path))
def test_normalize_cmd_PATH_shebang_PATH(in_tmpdir):
python = distutils.spawn.find_executable('python')
path = write_executable('/usr/bin/env python')
with bin_on_path():
ret = parse_shebang.normalize_cmd(('run',))
assert ret == (python, os.path.abspath(path))

View file

@ -78,7 +78,7 @@ def test_run_substitutes_prefix(popen_mock, makedirs_mock):
)
ret = instance.run(['{prefix}bar', 'baz'], retcode=None)
popen_mock.assert_called_once_with(
[five.n(os.path.join('prefix', 'bar')), five.n('baz')],
(five.n(os.path.join('prefix', 'bar')), five.n('baz')),
env=None,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
@ -132,4 +132,4 @@ def test_raises_on_error(popen_mock, makedirs_mock):
instance = PrefixedCommandRunner(
'.', popen=popen_mock, makedirs=makedirs_mock,
)
instance.run(['foo'])
instance.run(['echo'])