Activate conda environment in pre-commit hook.

Save conda environment that was active during conda install when using
option --hooks-activate-conda. The saved environment will be activated
before calling pre-commit hooks.

Especially on Windows, more and more actions within a conda environment
require the conda environment to be activated. Thus saving just the
python executable is not enough any more.

There is currently one downside of using the option
--hooks-activate-conda. It uses "conda run" which will only show
console output after the run is completed. We have a pull request to
conda open which introduces an option to show interactive console
output in conda run. Once this is approved, it might be ok to make this
option the default behaviour.
This commit is contained in:
Martin Trautmann 2020-02-10 16:56:29 +01:00
parent f0ee93c5a7
commit dbdba3c67f
4 changed files with 92 additions and 16 deletions

View file

@ -73,6 +73,7 @@ def _install_hook_script(
hook_type: str, hook_type: str,
overwrite: bool = False, overwrite: bool = False,
skip_on_missing_config: bool = False, skip_on_missing_config: bool = False,
hooks_activate_conda: bool = False,
git_dir: Optional[str] = None, git_dir: Optional[str] = None,
) -> None: ) -> None:
hook_path, legacy_path = _hook_paths(hook_type, git_dir=git_dir) hook_path, legacy_path = _hook_paths(hook_type, git_dir=git_dir)
@ -95,7 +96,20 @@ def _install_hook_script(
args = ['hook-impl', f'--config={config_file}', f'--hook-type={hook_type}'] args = ['hook-impl', f'--config={config_file}', f'--hook-type={hook_type}']
if skip_on_missing_config: if skip_on_missing_config:
args.append('--skip-on-missing-config') args.append('--skip-on-missing-config')
params = {'INSTALL_PYTHON': sys.executable, 'ARGS': args} params = {
'INSTALL_PYTHON': sys.executable, 'INSTALL_CONDA': '',
'INSTALL_CONDA_PREFIX': '', 'ARGS': args,
}
if hooks_activate_conda:
if 'CONDA_EXE' in os.environ and 'CONDA_PREFIX' in os.environ:
params['INSTALL_PYTHON'] = '' # conda will find correct python
params['INSTALL_CONDA'] = os.getenv('CONDA_EXE', '')
params['INSTALL_CONDA_PREFIX'] = os.getenv('CONDA_PREFIX', '')
else:
logger.warning(
'Failed to detect activated conda, '
'ignoring option --hooks_activate_conda.',
)
with open(hook_path, 'w') as hook_file: with open(hook_path, 'w') as hook_file:
contents = resource_text('hook-tmpl') contents = resource_text('hook-tmpl')
@ -121,6 +135,7 @@ def install(
overwrite: bool = False, overwrite: bool = False,
hooks: bool = False, hooks: bool = False,
skip_on_missing_config: bool = False, skip_on_missing_config: bool = False,
hooks_activate_conda: bool = False,
git_dir: Optional[str] = None, git_dir: Optional[str] = None,
) -> int: ) -> int:
if git_dir is None and git.has_core_hookpaths_set(): if git_dir is None and git.has_core_hookpaths_set():
@ -135,6 +150,7 @@ def install(
config_file, hook_type, config_file, hook_type,
overwrite=overwrite, overwrite=overwrite,
skip_on_missing_config=skip_on_missing_config, skip_on_missing_config=skip_on_missing_config,
hooks_activate_conda=hooks_activate_conda,
git_dir=git_dir, git_dir=git_dir,
) )

View file

@ -250,6 +250,13 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
'or exit with a failure code.' 'or exit with a failure code.'
), ),
) )
install_parser.add_argument(
'--hooks-activate-conda', action='store_true', default=False,
help=(
'Whether to activate conda environment before calling pre-commit'
'within hooks.'
),
)
install_hooks_parser = subparsers.add_parser( install_hooks_parser = subparsers.add_parser(
'install-hooks', 'install-hooks',
@ -357,6 +364,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
overwrite=args.overwrite, overwrite=args.overwrite,
hooks=args.install_hooks, hooks=args.install_hooks,
skip_on_missing_config=args.allow_missing_config, skip_on_missing_config=args.allow_missing_config,
hooks_activate_conda=args.hooks_activate_conda,
) )
elif args.command == 'init-templatedir': elif args.command == 'init-templatedir':
return init_templatedir( return init_templatedir(

View file

@ -18,6 +18,8 @@ os.environ.pop('__PYVENV_LAUNCHER__', None)
# start templated # start templated
INSTALL_PYTHON = '' INSTALL_PYTHON = ''
INSTALL_CONDA = ''
INSTALL_CONDA_PREFIX = ''
ARGS = ['hook-impl'] ARGS = ['hook-impl']
# end templated # end templated
ARGS.extend(('--hook-dir', os.path.realpath(os.path.dirname(__file__)))) ARGS.extend(('--hook-dir', os.path.realpath(os.path.dirname(__file__))))
@ -25,7 +27,9 @@ ARGS.append('--')
ARGS.extend(sys.argv[1:]) ARGS.extend(sys.argv[1:])
DNE = '`pre-commit` not found. Did you forget to activate your virtualenv?' DNE = '`pre-commit` not found. Did you forget to activate your virtualenv?'
if os.access(INSTALL_PYTHON, os.X_OK): if os.access(INSTALL_CONDA, os.X_OK):
CMD = [INSTALL_CONDA, 'run', '-p', INSTALL_CONDA_PREFIX, 'pre-commit']
elif os.access(INSTALL_PYTHON, os.X_OK):
CMD = [INSTALL_PYTHON, '-mpre_commit'] CMD = [INSTALL_PYTHON, '-mpre_commit']
elif which('pre-commit'): elif which('pre-commit'):
CMD = ['pre-commit'] CMD = ['pre-commit']

View file

@ -133,7 +133,9 @@ FILES_CHANGED = (
NORMAL_PRE_COMMIT_RUN = re.compile( NORMAL_PRE_COMMIT_RUN = re.compile(
fr'^\[INFO\] Initializing environment for .+\.\n' fr'^\[INFO\] Initializing environment for .+\.\n'
fr'Bash hook\.+Passed\n' fr'Bash hook\.+Passed\n'
fr'(\n)?'
fr'\[master [a-f0-9]{{7}}\] commit!\n' fr'\[master [a-f0-9]{{7}}\] commit!\n'
fr'( Author: .*\n)?'
fr'{FILES_CHANGED}' fr'{FILES_CHANGED}'
fr' create mode 100644 foo\n$', fr' create mode 100644 foo\n$',
) )
@ -487,6 +489,35 @@ def test_install_hooks_command(tempdir_factory, store):
assert PRE_INSTALLED.match(output) assert PRE_INSTALLED.match(output)
def get_environment_without_pre_commit():
return {
'HOME': os.path.expanduser('~'),
'PATH': _path_without_us(),
'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', ''),
# Git needs this to make a commit
'GIT_AUTHOR_NAME': os.getenv(
'GIT_AUTHOR_NAME',
'author_name',
),
'GIT_COMMITTER_NAME': os.getenv(
'GIT_COMMITTER_NAME',
'committer_name',
),
'GIT_AUTHOR_EMAIL': os.getenv(
'GIT_AUTHOR_EMAIL',
'author@his.email',
),
'GIT_COMMITTER_EMAIL': os.getenv(
'GIT_COMMITTER_EMAIL',
'committer@his.email',
),
}
def test_installed_from_venv(tempdir_factory, store): def test_installed_from_venv(tempdir_factory, store):
path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') path = make_consuming_repo(tempdir_factory, 'script_hooks_repo')
with cwd(path): with cwd(path):
@ -495,20 +526,37 @@ def test_installed_from_venv(tempdir_factory, store):
# Should still pick up the python from when we installed # Should still pick up the python from when we installed
ret, output = _get_commit_output( ret, output = _get_commit_output(
tempdir_factory, tempdir_factory,
env={ env=get_environment_without_pre_commit(),
'HOME': os.path.expanduser('~'), )
'PATH': _path_without_us(), assert ret == 0
'TERM': os.environ.get('TERM', ''), assert NORMAL_PRE_COMMIT_RUN.match(output)
# Windows needs this to import `random`
'SYSTEMROOT': os.environ.get('SYSTEMROOT', ''),
# Windows needs this to resolve executables def test_installed_from_conda_env(tempdir_factory, store):
'PATHEXT': os.environ.get('PATHEXT', ''), path = make_consuming_repo(tempdir_factory, 'script_hooks_repo')
# Git needs this to make a commit with cwd(path):
'GIT_AUTHOR_NAME': os.environ['GIT_AUTHOR_NAME'], conda_environment = tempdir_factory.get()
'GIT_COMMITTER_NAME': os.environ['GIT_COMMITTER_NAME'], # Create conda environment in tempdir and simulate environment
'GIT_AUTHOR_EMAIL': os.environ['GIT_AUTHOR_EMAIL'], # variables CONDA_EXE/PREFIX as if install() were called with
'GIT_COMMITTER_EMAIL': os.environ['GIT_COMMITTER_EMAIL'], # activated conda from tmpdir
}, cmd_output(
'conda', 'run', 'conda', 'create', '-p', conda_environment, '-y',
'-c', 'conda-forge', 'pre-commit',
)
os.environ['CONDA_EXE'] = cmd_output(
'conda', 'run', '-p', conda_environment,
'echo', '$CONDA_EXE',
)[1].strip()
os.environ['CONDA_PREFIX'] = conda_environment
install(
C.CONFIG_FILE, store, hook_types=['pre-commit'],
hooks_activate_conda=True,
)
# No environment so pre-commit is not on the path when running!
# Should still pick up the python from when we installed
ret, output = _get_commit_output(
tempdir_factory,
env=get_environment_without_pre_commit(),
) )
assert ret == 0 assert ret == 0
assert NORMAL_PRE_COMMIT_RUN.match(output) assert NORMAL_PRE_COMMIT_RUN.match(output)