1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367
|
from __future__ import annotations
import os.path
import sys
from unittest import mock
import pytest
import pre_commit.constants as C
from pre_commit.envcontext import envcontext
from pre_commit.languages import python
from pre_commit.prefix import Prefix
from pre_commit.store import _make_local_repo
from pre_commit.util import cmd_output_b
from pre_commit.util import make_executable
from pre_commit.util import win_exe
from testing.auto_namedtuple import auto_namedtuple
from testing.language_helpers import run_language
def test_read_pyvenv_cfg(tmpdir):
pyvenv_cfg = tmpdir.join('pyvenv.cfg')
pyvenv_cfg.write(
'# I am a comment\n'
'\n'
'foo = bar\n'
'version-info=123\n',
)
expected = {'foo': 'bar', 'version-info': '123'}
assert python._read_pyvenv_cfg(pyvenv_cfg) == expected
def test_read_pyvenv_cfg_non_utf8(tmpdir):
pyvenv_cfg = tmpdir.join('pyvenv_cfg')
pyvenv_cfg.write_binary('hello = hello john.š\n'.encode())
expected = {'hello': 'hello john.š'}
assert python._read_pyvenv_cfg(pyvenv_cfg) == expected
def _get_default_version(
*,
impl: str,
exe: str,
found: set[str],
version: tuple[int, int],
) -> str:
sys_exe = f'/fake/path/{exe}'
sys_impl = auto_namedtuple(name=impl)
sys_ver = auto_namedtuple(major=version[0], minor=version[1])
def find_exe(s):
if s in found:
return f'/fake/path/found/{exe}'
else:
return None
with (
mock.patch.object(sys, 'implementation', sys_impl),
mock.patch.object(sys, 'executable', sys_exe),
mock.patch.object(sys, 'version_info', sys_ver),
mock.patch.object(python, 'find_executable', find_exe),
):
return python.get_default_version.__wrapped__()
def test_default_version_sys_executable_found():
ret = _get_default_version(
impl='cpython',
exe='python3.12',
found={'python3.12'},
version=(3, 12),
)
assert ret == 'python3.12'
def test_default_version_picks_specific_when_found():
ret = _get_default_version(
impl='cpython',
exe='python3',
found={'python3', 'python3.12'},
version=(3, 12),
)
assert ret == 'python3.12'
def test_default_version_picks_pypy_versioned_exe():
ret = _get_default_version(
impl='pypy',
exe='python',
found={'pypy3.12', 'python3'},
version=(3, 12),
)
assert ret == 'pypy3.12'
def test_default_version_picks_pypy_unversioned_exe():
ret = _get_default_version(
impl='pypy',
exe='python',
found={'pypy3', 'python3'},
version=(3, 12),
)
assert ret == 'pypy3'
def test_norm_version_expanduser():
home = os.path.expanduser('~')
if sys.platform == 'win32': # pragma: win32 cover
path = r'~\python343'
expected_path = fr'{home}\python343'
else: # pragma: win32 no cover
path = '~/.pyenv/versions/3.4.3/bin/python'
expected_path = f'{home}/.pyenv/versions/3.4.3/bin/python'
result = python.norm_version(path)
assert result == expected_path
def test_norm_version_of_default_is_sys_executable():
assert python.norm_version('default') is None
@pytest.mark.parametrize('v', ('python3.9', 'python3', 'python'))
def test_sys_executable_matches(v):
with mock.patch.object(sys, 'version_info', (3, 9, 10)):
assert python._sys_executable_matches(v)
assert python.norm_version(v) is None
@pytest.mark.parametrize('v', ('notpython', 'python3.x'))
def test_sys_executable_matches_does_not_match(v):
with mock.patch.object(sys, 'version_info', (3, 9, 10)):
assert not python._sys_executable_matches(v)
@pytest.mark.parametrize(
('exe', 'realpath', 'expected'), (
('/usr/bin/python3', '/usr/bin/python3.7', 'python3'),
('/usr/bin/python', '/usr/bin/python3.7', 'python3.7'),
('/usr/bin/python', '/usr/bin/python', None),
('/usr/bin/python3.7m', '/usr/bin/python3.7m', 'python3.7m'),
('v/bin/python', 'v/bin/pypy', 'pypy'),
),
)
def test_find_by_sys_executable(exe, realpath, expected):
with mock.patch.object(sys, 'executable', exe):
with mock.patch.object(os.path, 'realpath', return_value=realpath):
with mock.patch.object(python, 'find_executable', lambda x: x):
assert python._find_by_sys_executable() == expected
@pytest.fixture
def python_dir(tmpdir):
with tmpdir.as_cwd():
prefix = tmpdir.join('prefix').ensure_dir()
prefix.join('setup.py').write('import setuptools; setuptools.setup()')
prefix = Prefix(str(prefix))
yield prefix, tmpdir
def test_healthy_default_creator(python_dir):
prefix, tmpdir = python_dir
python.install_environment(prefix, C.DEFAULT, ())
# should be healthy right after creation
assert python.health_check(prefix, C.DEFAULT) is None
# even if a `types.py` file exists, should still be healthy
tmpdir.join('types.py').ensure()
assert python.health_check(prefix, C.DEFAULT) is None
def test_healthy_venv_creator(python_dir):
# venv creator produces slightly different pyvenv.cfg
prefix, tmpdir = python_dir
with envcontext((('VIRTUALENV_CREATOR', 'venv'),)):
python.install_environment(prefix, C.DEFAULT, ())
assert python.health_check(prefix, C.DEFAULT) is None
def test_unhealthy_python_goes_missing(python_dir):
prefix, tmpdir = python_dir
python.install_environment(prefix, C.DEFAULT, ())
exe_name = win_exe('python')
py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name)
os.remove(py_exe)
ret = python.health_check(prefix, C.DEFAULT)
assert ret == (
f'virtualenv python version did not match created version:\n'
f'- actual version: <<error retrieving version from {py_exe}>>\n'
f'- expected version: {python._version_info(sys.executable)}\n'
)
def test_unhealthy_with_version_change(python_dir):
prefix, tmpdir = python_dir
python.install_environment(prefix, C.DEFAULT, ())
with open(prefix.path('py_env-default/pyvenv.cfg'), 'a+') as f:
f.write('version_info = 1.2.3\n')
ret = python.health_check(prefix, C.DEFAULT)
assert ret == (
f'virtualenv python version did not match created version:\n'
f'- actual version: {python._version_info(sys.executable)}\n'
f'- expected version: 1.2.3\n'
)
def test_unhealthy_system_version_changes(python_dir):
prefix, tmpdir = python_dir
python.install_environment(prefix, C.DEFAULT, ())
with open(prefix.path('py_env-default/pyvenv.cfg'), 'a') as f:
f.write('base-executable = /does/not/exist\n')
ret = python.health_check(prefix, C.DEFAULT)
assert ret == (
f'base executable python version does not match created version:\n'
f'- base-executable version: <<error retrieving version from /does/not/exist>>\n' # noqa: E501
f'- expected version: {python._version_info(sys.executable)}\n'
)
def test_unhealthy_old_virtualenv(python_dir):
prefix, tmpdir = python_dir
python.install_environment(prefix, C.DEFAULT, ())
# simulate "old" virtualenv by deleting this file
os.remove(prefix.path('py_env-default/pyvenv.cfg'))
ret = python.health_check(prefix, C.DEFAULT)
assert ret == 'pyvenv.cfg does not exist (old virtualenv?)'
def test_unhealthy_unexpected_pyvenv(python_dir):
prefix, tmpdir = python_dir
python.install_environment(prefix, C.DEFAULT, ())
# simulate a buggy environment build (I don't think this is possible)
with open(prefix.path('py_env-default/pyvenv.cfg'), 'w'):
pass
ret = python.health_check(prefix, C.DEFAULT)
assert ret == "created virtualenv's pyvenv.cfg is missing `version_info`"
def test_unhealthy_then_replaced(python_dir):
prefix, tmpdir = python_dir
python.install_environment(prefix, C.DEFAULT, ())
# simulate an exe which returns an old version
exe_name = win_exe('python')
py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name)
os.rename(py_exe, f'{py_exe}.tmp')
with open(py_exe, 'w') as f:
f.write('#!/usr/bin/env bash\necho 1.2.3\n')
make_executable(py_exe)
# should be unhealthy due to version mismatch
ret = python.health_check(prefix, C.DEFAULT)
assert ret == (
f'virtualenv python version did not match created version:\n'
f'- actual version: 1.2.3\n'
f'- expected version: {python._version_info(sys.executable)}\n'
)
# now put the exe back and it should be healthy again
os.replace(f'{py_exe}.tmp', py_exe)
assert python.health_check(prefix, C.DEFAULT) is None
def test_language_versioned_python_hook(tmp_path):
setup_py = '''\
from setuptools import setup
setup(
name='example',
py_modules=['mod'],
entry_points={'console_scripts': ['myexe=mod:main']},
)
'''
tmp_path.joinpath('setup.py').write_text(setup_py)
tmp_path.joinpath('mod.py').write_text('def main(): print("ohai")')
# we patch this to force virtualenv executing with `-p` since we can't
# reliably have multiple pythons available in CI
with mock.patch.object(
python,
'_sys_executable_matches',
return_value=False,
):
assert run_language(tmp_path, python, 'myexe') == (0, b'ohai\n')
def _make_hello_hello(tmp_path):
setup_py = '''\
from setuptools import setup
setup(
name='socks',
version='0.0.0',
py_modules=['socks'],
entry_points={'console_scripts': ['socks = socks:main']},
)
'''
main_py = '''\
import sys
def main():
print(repr(sys.argv[1:]))
print('hello hello')
return 0
'''
tmp_path.joinpath('setup.py').write_text(setup_py)
tmp_path.joinpath('socks.py').write_text(main_py)
def test_simple_python_hook(tmp_path):
_make_hello_hello(tmp_path)
ret = run_language(tmp_path, python, 'socks', [os.devnull])
assert ret == (0, f'[{os.devnull!r}]\nhello hello\n'.encode())
def test_simple_python_hook_default_version(tmp_path):
# make sure that this continues to work for platforms where default
# language detection does not work
with mock.patch.object(
python,
'get_default_version',
return_value=C.DEFAULT,
):
test_simple_python_hook(tmp_path)
def test_python_hook_weird_setup_cfg(tmp_path):
_make_hello_hello(tmp_path)
setup_cfg = '[install]\ninstall_scripts=/usr/sbin'
tmp_path.joinpath('setup.cfg').write_text(setup_cfg)
ret = run_language(tmp_path, python, 'socks', [os.devnull])
assert ret == (0, f'[{os.devnull!r}]\nhello hello\n'.encode())
def test_local_repo_with_other_artifacts(tmp_path):
cmd_output_b('git', 'init', tmp_path)
_make_local_repo(str(tmp_path))
# pretend a rust install also ran here
tmp_path.joinpath('target').mkdir()
ret, out = run_language(tmp_path, python, 'python --version')
assert ret == 0
assert out.startswith(b'Python ')
|