From dbdba3c67fb89c5ece7251406aac82a4a8306768 Mon Sep 17 00:00:00 2001 From: Martin Trautmann Date: Mon, 10 Feb 2020 16:56:29 +0100 Subject: [PATCH] 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. --- pre_commit/commands/install_uninstall.py | 18 +++++- pre_commit/main.py | 8 +++ pre_commit/resources/hook-tmpl | 6 +- tests/commands/install_uninstall_test.py | 76 +++++++++++++++++++----- 4 files changed, 92 insertions(+), 16 deletions(-) diff --git a/pre_commit/commands/install_uninstall.py b/pre_commit/commands/install_uninstall.py index b2ccc5cf..a122fb28 100644 --- a/pre_commit/commands/install_uninstall.py +++ b/pre_commit/commands/install_uninstall.py @@ -73,6 +73,7 @@ def _install_hook_script( hook_type: str, overwrite: bool = False, skip_on_missing_config: bool = False, + hooks_activate_conda: bool = False, git_dir: Optional[str] = None, ) -> None: 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}'] if 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: contents = resource_text('hook-tmpl') @@ -121,6 +135,7 @@ def install( overwrite: bool = False, hooks: bool = False, skip_on_missing_config: bool = False, + hooks_activate_conda: bool = False, git_dir: Optional[str] = None, ) -> int: if git_dir is None and git.has_core_hookpaths_set(): @@ -135,6 +150,7 @@ def install( config_file, hook_type, overwrite=overwrite, skip_on_missing_config=skip_on_missing_config, + hooks_activate_conda=hooks_activate_conda, git_dir=git_dir, ) diff --git a/pre_commit/main.py b/pre_commit/main.py index 1d849c05..c8840b5c 100644 --- a/pre_commit/main.py +++ b/pre_commit/main.py @@ -250,6 +250,13 @@ def main(argv: Optional[Sequence[str]] = None) -> int: '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', @@ -357,6 +364,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: overwrite=args.overwrite, hooks=args.install_hooks, skip_on_missing_config=args.allow_missing_config, + hooks_activate_conda=args.hooks_activate_conda, ) elif args.command == 'init-templatedir': return init_templatedir( diff --git a/pre_commit/resources/hook-tmpl b/pre_commit/resources/hook-tmpl index 299144ec..a9ef8a76 100755 --- a/pre_commit/resources/hook-tmpl +++ b/pre_commit/resources/hook-tmpl @@ -18,6 +18,8 @@ os.environ.pop('__PYVENV_LAUNCHER__', None) # start templated INSTALL_PYTHON = '' +INSTALL_CONDA = '' +INSTALL_CONDA_PREFIX = '' ARGS = ['hook-impl'] # end templated ARGS.extend(('--hook-dir', os.path.realpath(os.path.dirname(__file__)))) @@ -25,7 +27,9 @@ ARGS.append('--') ARGS.extend(sys.argv[1:]) 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'] elif which('pre-commit'): CMD = ['pre-commit'] diff --git a/tests/commands/install_uninstall_test.py b/tests/commands/install_uninstall_test.py index 6d486149..d90964ab 100644 --- a/tests/commands/install_uninstall_test.py +++ b/tests/commands/install_uninstall_test.py @@ -133,7 +133,9 @@ FILES_CHANGED = ( NORMAL_PRE_COMMIT_RUN = re.compile( fr'^\[INFO\] Initializing environment for .+\.\n' fr'Bash hook\.+Passed\n' + fr'(\n)?' fr'\[master [a-f0-9]{{7}}\] commit!\n' + fr'( Author: .*\n)?' fr'{FILES_CHANGED}' fr' create mode 100644 foo\n$', ) @@ -487,6 +489,35 @@ def test_install_hooks_command(tempdir_factory, store): 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): path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') 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 ret, output = _get_commit_output( tempdir_factory, - env={ - '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.environ['GIT_AUTHOR_NAME'], - 'GIT_COMMITTER_NAME': os.environ['GIT_COMMITTER_NAME'], - 'GIT_AUTHOR_EMAIL': os.environ['GIT_AUTHOR_EMAIL'], - 'GIT_COMMITTER_EMAIL': os.environ['GIT_COMMITTER_EMAIL'], - }, + env=get_environment_without_pre_commit(), + ) + assert ret == 0 + assert NORMAL_PRE_COMMIT_RUN.match(output) + + +def test_installed_from_conda_env(tempdir_factory, store): + path = make_consuming_repo(tempdir_factory, 'script_hooks_repo') + with cwd(path): + conda_environment = tempdir_factory.get() + # Create conda environment in tempdir and simulate environment + # variables CONDA_EXE/PREFIX as if install() were called with + # 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 NORMAL_PRE_COMMIT_RUN.match(output)