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)