Skip to content

Commit 3d50b37

Browse files
committed
Improve python healthy() and eliminate python_venv
- the `healthy()` check now requires virtualenv 20.x's metadata - `python_venv` is obsolete now that `virtualenv` generates the same structure and `virtualenv` is more portable
1 parent 5ed3f56 commit 3d50b37

File tree

8 files changed

+163
-145
lines changed

8 files changed

+163
-145
lines changed

pre_commit/languages/all.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
from pre_commit.languages import perl
1515
from pre_commit.languages import pygrep
1616
from pre_commit.languages import python
17-
from pre_commit.languages import python_venv
1817
from pre_commit.languages import ruby
1918
from pre_commit.languages import rust
2019
from pre_commit.languages import script
@@ -49,12 +48,13 @@ class Language(NamedTuple):
4948
'perl': Language(name='perl', ENVIRONMENT_DIR=perl.ENVIRONMENT_DIR, get_default_version=perl.get_default_version, healthy=perl.healthy, install_environment=perl.install_environment, run_hook=perl.run_hook), # noqa: E501
5049
'pygrep': Language(name='pygrep', ENVIRONMENT_DIR=pygrep.ENVIRONMENT_DIR, get_default_version=pygrep.get_default_version, healthy=pygrep.healthy, install_environment=pygrep.install_environment, run_hook=pygrep.run_hook), # noqa: E501
5150
'python': Language(name='python', ENVIRONMENT_DIR=python.ENVIRONMENT_DIR, get_default_version=python.get_default_version, healthy=python.healthy, install_environment=python.install_environment, run_hook=python.run_hook), # noqa: E501
52-
'python_venv': Language(name='python_venv', ENVIRONMENT_DIR=python_venv.ENVIRONMENT_DIR, get_default_version=python_venv.get_default_version, healthy=python_venv.healthy, install_environment=python_venv.install_environment, run_hook=python_venv.run_hook), # noqa: E501
5351
'ruby': Language(name='ruby', ENVIRONMENT_DIR=ruby.ENVIRONMENT_DIR, get_default_version=ruby.get_default_version, healthy=ruby.healthy, install_environment=ruby.install_environment, run_hook=ruby.run_hook), # noqa: E501
5452
'rust': Language(name='rust', ENVIRONMENT_DIR=rust.ENVIRONMENT_DIR, get_default_version=rust.get_default_version, healthy=rust.healthy, install_environment=rust.install_environment, run_hook=rust.run_hook), # noqa: E501
5553
'script': Language(name='script', ENVIRONMENT_DIR=script.ENVIRONMENT_DIR, get_default_version=script.get_default_version, healthy=script.healthy, install_environment=script.install_environment, run_hook=script.run_hook), # noqa: E501
5654
'swift': Language(name='swift', ENVIRONMENT_DIR=swift.ENVIRONMENT_DIR, get_default_version=swift.get_default_version, healthy=swift.healthy, install_environment=swift.install_environment, run_hook=swift.run_hook), # noqa: E501
5755
'system': Language(name='system', ENVIRONMENT_DIR=system.ENVIRONMENT_DIR, get_default_version=system.get_default_version, healthy=system.healthy, install_environment=system.install_environment, run_hook=system.run_hook), # noqa: E501
5856
# END GENERATED
5957
}
58+
# TODO: fully deprecate `python_venv`
59+
languages['python_venv'] = languages['python']
6060
all_languages = sorted(languages)

pre_commit/languages/python.py

Lines changed: 77 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
import functools
33
import os
44
import sys
5-
from typing import Callable
6-
from typing import ContextManager
5+
from typing import Dict
76
from typing import Generator
87
from typing import Optional
98
from typing import Sequence
@@ -26,6 +25,28 @@
2625
ENVIRONMENT_DIR = 'py_env'
2726

2827

28+
@functools.lru_cache(maxsize=None)
29+
def _version_info(exe: str) -> str:
30+
prog = 'import sys;print(".".join(str(p) for p in sys.version_info))'
31+
try:
32+
return cmd_output(exe, '-S', '-c', prog)[1].strip()
33+
except CalledProcessError:
34+
return f'<<error retrieving version from {exe}>>'
35+
36+
37+
def _read_pyvenv_cfg(filename: str) -> Dict[str, str]:
38+
ret = {}
39+
with open(filename) as f:
40+
for line in f:
41+
try:
42+
k, v = line.split('=')
43+
except ValueError: # blank line / comment / etc.
44+
continue
45+
else:
46+
ret[k.strip()] = v.strip()
47+
return ret
48+
49+
2950
def bin_dir(venv: str) -> str:
3051
"""On windows there's a different directory for the virtualenv"""
3152
bin_part = 'Scripts' if os.name == 'nt' else 'bin'
@@ -116,6 +137,9 @@ def _sys_executable_matches(version: str) -> bool:
116137

117138

118139
def norm_version(version: str) -> str:
140+
if version == C.DEFAULT:
141+
return os.path.realpath(sys.executable)
142+
119143
# first see if our current executable is appropriate
120144
if _sys_executable_matches(version):
121145
return sys.executable
@@ -140,70 +164,59 @@ def norm_version(version: str) -> str:
140164
return os.path.expanduser(version)
141165

142166

143-
def py_interface(
144-
_dir: str,
145-
_make_venv: Callable[[str, str], None],
146-
) -> Tuple[
147-
Callable[[Prefix, str], ContextManager[None]],
148-
Callable[[Prefix, str], bool],
149-
Callable[[Hook, Sequence[str], bool], Tuple[int, bytes]],
150-
Callable[[Prefix, str, Sequence[str]], None],
151-
]:
152-
@contextlib.contextmanager
153-
def in_env(
154-
prefix: Prefix,
155-
language_version: str,
156-
) -> Generator[None, None, None]:
157-
envdir = prefix.path(helpers.environment_dir(_dir, language_version))
158-
with envcontext(get_env_patch(envdir)):
159-
yield
160-
161-
def healthy(prefix: Prefix, language_version: str) -> bool:
162-
envdir = helpers.environment_dir(_dir, language_version)
163-
exe_name = 'python.exe' if sys.platform == 'win32' else 'python'
164-
py_exe = prefix.path(bin_dir(envdir), exe_name)
165-
with in_env(prefix, language_version):
166-
retcode, _, _ = cmd_output_b(
167-
py_exe, '-c', 'import ctypes, datetime, io, os, ssl, weakref',
168-
cwd='/',
169-
retcode=None,
170-
)
171-
return retcode == 0
172-
173-
def run_hook(
174-
hook: Hook,
175-
file_args: Sequence[str],
176-
color: bool,
177-
) -> Tuple[int, bytes]:
178-
with in_env(hook.prefix, hook.language_version):
179-
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
180-
181-
def install_environment(
182-
prefix: Prefix,
183-
version: str,
184-
additional_dependencies: Sequence[str],
185-
) -> None:
186-
directory = helpers.environment_dir(_dir, version)
187-
install = ('python', '-mpip', 'install', '.', *additional_dependencies)
188-
189-
env_dir = prefix.path(directory)
190-
with clean_path_on_failure(env_dir):
191-
if version != C.DEFAULT:
192-
python = norm_version(version)
193-
else:
194-
python = os.path.realpath(sys.executable)
195-
_make_venv(env_dir, python)
196-
with in_env(prefix, version):
197-
helpers.run_setup_cmd(prefix, install)
167+
@contextlib.contextmanager
168+
def in_env(
169+
prefix: Prefix,
170+
language_version: str,
171+
) -> Generator[None, None, None]:
172+
directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version)
173+
envdir = prefix.path(directory)
174+
with envcontext(get_env_patch(envdir)):
175+
yield
198176

199-
return in_env, healthy, run_hook, install_environment
200177

178+
def healthy(prefix: Prefix, language_version: str) -> bool:
179+
directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version)
180+
envdir = prefix.path(directory)
181+
pyvenv_cfg = os.path.join(envdir, 'pyvenv.cfg')
201182

202-
def make_venv(envdir: str, python: str) -> None:
203-
env = dict(os.environ, VIRTUALENV_NO_DOWNLOAD='1')
204-
cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python)
205-
cmd_output_b(*cmd, env=env, cwd='/')
183+
# created with "old" virtualenv
184+
if not os.path.exists(pyvenv_cfg):
185+
return False
186+
187+
exe_name = 'python.exe' if sys.platform == 'win32' else 'python'
188+
py_exe = prefix.path(bin_dir(envdir), exe_name)
189+
cfg = _read_pyvenv_cfg(pyvenv_cfg)
190+
191+
return (
192+
'version_info' in cfg and
193+
_version_info(py_exe) == cfg['version_info'] and (
194+
'base-executable' not in cfg or
195+
_version_info(cfg['base-executable']) == cfg['version_info']
196+
)
197+
)
206198

207199

208-
_interface = py_interface(ENVIRONMENT_DIR, make_venv)
209-
in_env, healthy, run_hook, install_environment = _interface
200+
def install_environment(
201+
prefix: Prefix,
202+
version: str,
203+
additional_dependencies: Sequence[str],
204+
) -> None:
205+
envdir = prefix.path(helpers.environment_dir(ENVIRONMENT_DIR, version))
206+
python = norm_version(version)
207+
venv_cmd = (sys.executable, '-mvirtualenv', envdir, '-p', python)
208+
install_cmd = ('python', '-mpip', 'install', '.', *additional_dependencies)
209+
210+
with clean_path_on_failure(envdir):
211+
cmd_output_b(*venv_cmd, cwd='/')
212+
with in_env(prefix, version):
213+
helpers.run_setup_cmd(prefix, install_cmd)
214+
215+
216+
def run_hook(
217+
hook: Hook,
218+
file_args: Sequence[str],
219+
color: bool,
220+
) -> Tuple[int, bytes]:
221+
with in_env(hook.prefix, hook.language_version):
222+
return helpers.run_xargs(hook, hook.cmd, file_args, color=color)

pre_commit/languages/python_venv.py

Lines changed: 0 additions & 46 deletions
This file was deleted.

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ install_requires =
2727
nodeenv>=0.11.1
2828
pyyaml>=5.1
2929
toml
30-
virtualenv>=15.2
30+
virtualenv>=20.0.8
3131
importlib-metadata;python_version<"3.8"
3232
importlib-resources;python_version<"3.7"
3333
python_requires = >=3.6.1

testing/gen-languages-all

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ import sys
33

44
LANGUAGES = [
55
'conda', 'docker', 'docker_image', 'fail', 'golang', 'node', 'perl',
6-
'pygrep', 'python', 'python_venv', 'ruby', 'rust', 'script', 'swift',
7-
'system',
6+
'pygrep', 'python', 'ruby', 'rust', 'script', 'swift', 'system',
87
]
98
FIELDS = [
109
'ENVIRONMENT_DIR', 'get_default_version', 'healthy', 'install_environment',

testing/util.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,6 @@ def cmd_output_mocked_pre_commit_home(
4545
xfailif_windows = pytest.mark.xfail(os.name == 'nt', reason='windows')
4646

4747

48-
def supports_venv(): # pragma: no cover (platform specific)
49-
try:
50-
__import__('ensurepip')
51-
__import__('venv')
52-
return True
53-
except ImportError:
54-
return False
55-
56-
57-
xfailif_no_venv = pytest.mark.xfail(
58-
not supports_venv(), reason='Does not support venv module',
59-
)
60-
61-
6248
def run_opts(
6349
all_files=False,
6450
files=(),

0 commit comments

Comments
 (0)