import contextlib import functools import os import sys from typing import Callable from typing import ContextManager from typing import Generator from typing import Optional from typing import Sequence from typing import Tuple import pre_commit.constants as C from pre_commit.envcontext import envcontext from pre_commit.envcontext import PatchesT from pre_commit.envcontext import UNSET from pre_commit.envcontext import Var from pre_commit.hook import Hook from pre_commit.languages import helpers from pre_commit.parse_shebang import find_executable from pre_commit.prefix import Prefix from pre_commit.util import CalledProcessError from pre_commit.util import clean_path_on_failure from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b ENVIRONMENT_DIR = 'py_env' def bin_dir(venv: str) -> str: """On windows there's a different directory for the virtualenv""" bin_part = 'Scripts' if os.name == 'nt' else 'bin' return os.path.join(venv, bin_part) def get_env_patch(venv: str) -> PatchesT: return ( ('PYTHONHOME', UNSET), ('VIRTUAL_ENV', venv), ('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))), ) def _find_by_py_launcher( version: str, ) -> Optional[str]: # pragma: no cover (windows only) if version.startswith('python'): num = version[len('python'):] try: cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)') return cmd_output(*cmd)[1].strip() except CalledProcessError: pass return None def _find_by_sys_executable() -> Optional[str]: def _norm(path: str) -> Optional[str]: _, exe = os.path.split(path.lower()) exe, _, _ = exe.partition('.exe') if exe not in {'python', 'pythonw'} and find_executable(exe): return exe return None # On linux, I see these common sys.executables: # # system `python`: /usr/bin/python -> python2.7 # system `python2`: /usr/bin/python2 -> python2.7 # virtualenv v: v/bin/python (will not return from this loop) # virtualenv v -ppython2: v/bin/python -> python2 # virtualenv v -ppython2.7: v/bin/python -> python2.7 # virtualenv v -ppypy: v/bin/python -> v/bin/pypy for path in (sys.executable, os.path.realpath(sys.executable)): exe = _norm(path) if exe: return exe return None @functools.lru_cache(maxsize=1) def get_default_version() -> str: # pragma: no cover (platform dependent) # First attempt from `sys.executable` (or the realpath) exe = _find_by_sys_executable() if exe: return exe # Next try the `pythonX.X` executable exe = f'python{sys.version_info[0]}.{sys.version_info[1]}' if find_executable(exe): return exe if _find_by_py_launcher(exe): return exe # Give a best-effort try for windows default_folder_name = exe.replace('.', '') if os.path.exists(fr'C:\{default_folder_name}\python.exe'): return exe # We tried! return C.DEFAULT def _sys_executable_matches(version: str) -> bool: if version == 'python': return True elif not version.startswith('python'): return False try: info = tuple(int(p) for p in version[len('python'):].split('.')) except ValueError: return False return sys.version_info[:len(info)] == info def norm_version(version: str) -> str: # first see if our current executable is appropriate if _sys_executable_matches(version): return sys.executable if os.name == 'nt': # pragma: no cover (windows) version_exec = _find_by_py_launcher(version) if version_exec: return version_exec # Try looking up by name version_exec = find_executable(version) if version_exec and version_exec != version: return version_exec # If it is in the form pythonx.x search in the default # place on windows if version.startswith('python'): default_folder_name = version.replace('.', '') return fr'C:\{default_folder_name}\python.exe' # Otherwise assume it is a path return os.path.expanduser(version) def py_interface( _dir: str, _make_venv: Callable[[str, str], None], ) -> Tuple[ Callable[[Prefix, str], ContextManager[None]], Callable[[Prefix, str], bool], Callable[[Hook, Sequence[str], bool], Tuple[int, bytes]], Callable[[Prefix, str, Sequence[str]], None], ]: @contextlib.contextmanager def in_env( prefix: Prefix, language_version: str, ) -> Generator[None, None, None]: envdir = prefix.path(helpers.environment_dir(_dir, language_version)) with envcontext(get_env_patch(envdir)): yield def healthy(prefix: Prefix, language_version: str) -> bool: envdir = helpers.environment_dir(_dir, language_version) exe_name = 'python.exe' if sys.platform == 'win32' else 'python' py_exe = prefix.path(bin_dir(envdir), exe_name) with in_env(prefix, language_version): retcode, _, _ = cmd_output_b( py_exe, '-c', 'import ctypes, datetime, io, os, ssl, weakref', cwd='/', retcode=None, ) return retcode == 0 def run_hook( hook: Hook, file_args: Sequence[str], color: bool, ) -> Tuple[int, bytes]: with in_env(hook.prefix, hook.language_version): return helpers.run_xargs(hook, hook.cmd, file_args, color=color) def install_environment( prefix: Prefix, version: str, additional_dependencies: Sequence[str], ) -> None: directory = helpers.environment_dir(_dir, version) install = ('python', '-mpip', 'install', '.', *additional_dependencies) env_dir = prefix.path(directory) with clean_path_on_failure(env_dir): if version != C.DEFAULT: python = norm_version(version) else: python = os.path.realpath(sys.executable) _make_venv(env_dir, python) with in_env(prefix, version): helpers.run_setup_cmd(prefix, install) return in_env, healthy, run_hook, install_environment def make_venv(envdir: str, python: str) -> None: env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1') cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python) cmd_output_b(*cmd, env=env, cwd='/') _interface = py_interface(ENVIRONMENT_DIR, make_venv) in_env, healthy, run_hook, install_environment = _interface