summaryrefslogtreecommitdiffstats
path: root/pre_commit/languages/python.py
diff options
context:
space:
mode:
Diffstat (limited to 'pre_commit/languages/python.py')
-rw-r--r--pre_commit/languages/python.py147
1 files changed, 81 insertions, 66 deletions
diff --git a/pre_commit/languages/python.py b/pre_commit/languages/python.py
index 85d8281..6f7c900 100644
--- a/pre_commit/languages/python.py
+++ b/pre_commit/languages/python.py
@@ -2,8 +2,7 @@ import contextlib
import functools
import os
import sys
-from typing import Callable
-from typing import ContextManager
+from typing import Dict
from typing import Generator
from typing import Optional
from typing import Sequence
@@ -26,6 +25,28 @@ from pre_commit.util import cmd_output_b
ENVIRONMENT_DIR = 'py_env'
+@functools.lru_cache(maxsize=None)
+def _version_info(exe: str) -> str:
+ prog = 'import sys;print(".".join(str(p) for p in sys.version_info))'
+ try:
+ return cmd_output(exe, '-S', '-c', prog)[1].strip()
+ except CalledProcessError:
+ return f'<<error retrieving version from {exe}>>'
+
+
+def _read_pyvenv_cfg(filename: str) -> Dict[str, str]:
+ ret = {}
+ with open(filename) as f:
+ for line in f:
+ try:
+ k, v = line.split('=')
+ except ValueError: # blank line / comment / etc.
+ continue
+ else:
+ ret[k.strip()] = v.strip()
+ return ret
+
+
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'
@@ -34,6 +55,7 @@ def bin_dir(venv: str) -> str:
def get_env_patch(venv: str) -> PatchesT:
return (
+ ('PIP_DISABLE_PIP_VERSION_CHECK', '1'),
('PYTHONHOME', UNSET),
('VIRTUAL_ENV', venv),
('PATH', (bin_dir(venv), os.pathsep, Var('PATH'))),
@@ -45,9 +67,10 @@ def _find_by_py_launcher(
) -> Optional[str]: # pragma: no cover (windows only)
if version.startswith('python'):
num = version[len('python'):]
+ cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)')
+ env = dict(os.environ, PYTHONIOENCODING='UTF-8')
try:
- cmd = ('py', f'-{num}', '-c', 'import sys; print(sys.executable)')
- return cmd_output(*cmd)[1].strip()
+ return cmd_output(*cmd, env=env)[1].strip()
except CalledProcessError:
pass
return None
@@ -115,6 +138,9 @@ def _sys_executable_matches(version: str) -> bool:
def norm_version(version: str) -> str:
+ if version == C.DEFAULT:
+ return os.path.realpath(sys.executable)
+
# first see if our current executable is appropriate
if _sys_executable_matches(version):
return sys.executable
@@ -139,70 +165,59 @@ def norm_version(version: str) -> str:
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)
+@contextlib.contextmanager
+def in_env(
+ prefix: Prefix,
+ language_version: str,
+) -> Generator[None, None, None]:
+ directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version)
+ envdir = prefix.path(directory)
+ with envcontext(get_env_patch(envdir)):
+ yield
- return in_env, healthy, run_hook, install_environment
+def healthy(prefix: Prefix, language_version: str) -> bool:
+ directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version)
+ envdir = prefix.path(directory)
+ pyvenv_cfg = os.path.join(envdir, 'pyvenv.cfg')
-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='/')
+ # created with "old" virtualenv
+ if not os.path.exists(pyvenv_cfg):
+ return False
+
+ exe_name = 'python.exe' if sys.platform == 'win32' else 'python'
+ py_exe = prefix.path(bin_dir(envdir), exe_name)
+ cfg = _read_pyvenv_cfg(pyvenv_cfg)
+
+ return (
+ 'version_info' in cfg and
+ _version_info(py_exe) == cfg['version_info'] and (
+ 'base-executable' not in cfg or
+ _version_info(cfg['base-executable']) == cfg['version_info']
+ )
+ )
-_interface = py_interface(ENVIRONMENT_DIR, make_venv)
-in_env, healthy, run_hook, install_environment = _interface
+def install_environment(
+ prefix: Prefix,
+ version: str,
+ additional_dependencies: Sequence[str],
+) -> None:
+ envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version))
+ python = norm_version(version)
+ venv_cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python)
+ install_cmd = ('python', '-mpip', 'install', '.', *additional_dependencies)
+
+ with clean_path_on_failure(envdir):
+ cmd_output_b(*venv_cmd, cwd='/')
+ with in_env(prefix, version):
+ helpers.run_setup_cmd(prefix, install_cmd)
+
+
+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)