Repository: pre-commit/pre-commit

Issue:
Support for conda as a language
It would be nice to have support for conda in `pre-commit` as a "language"/environment manager. This would enable setups where packages are installed through conda. The typical usage for this will be for Python and R setups (e.g. https://github.com/lorenzwalthert/precommit/issues/99) where conda is most popular but as `conda` is language independent.

The implementation would work as follows:

- A hook is expected to have a requirements.txt that is installed using `conda create -p <prefix> --file=requirements.txt`. I would not use the conda `environment.yml` as this already dictates the name of the environment.
- The hook is run using `conda run -p <prefix> <entry>`
- We will use the `conda` executable that is available on the `PATH`.

Note: I would volunteer to implement this once there are some 👍 that the proposal is sound.


Discussion:
seems fine, you can go ahead and start implementing if you'd like -- a few questions:

- isn't `conda create -f environment.yml` more standard?
- can we avoid `conda run` and just use `PATH` to run the executables (this'll be faster and is more consistent with the current `language`s)?
- how does this interact with `language_version`? (this can be ignored for the first pass implementation)
- how does this interact with `additional_dependencies`? (this can be ignored for the first pass implementation)
- should we bootstrap `conda` for systems where it isn't installed (this can be ignored for the first pass implementation and just depend on system-installed `conda`)

more docs on adding a language are [here](https://github.com/pre-commit/pre-commit/blob/master/CONTRIBUTING.md#adding-support-for-a-new-hook-language)
> isn't conda create -f environment.yml more standard?

I wouldn't say either is standard.  It's just whichever fits your workflow best.  environment.yml allows you to specify channels and pip packages in a way that the other approach does not.

The name in the environment.yml file can be overridden by passing an env name to the conda env create command, so don't let that block you from using it.

> can we avoid conda run and just use PATH to run the executables (this'll be faster and is more consistent with the current languages)?

That'll work for probably about 98% of the time.  Anything that requires environment variables to be set (which is done via shell scripts that are sourced by activation) won't work.  There's a few notable things here - java being the main concern with JAVAHOME.

> should we bootstrap conda for systems where it isn't installed (this can be ignored for the first pass implementation and just depend on system-installed conda)

For installation purposes, you might want to use the standalone executables: https://repo.anaconda.com/pkgs/misc/conda-execs/

These won't work for activation, but if you only do the PATH modification, it doesn't matter.
> * isn't `conda create -f environment.yml` more standard?

As mentioned above, this sadly forces a name, thus I'm omitting it. Still, I posted this in the conda gitter with the hope of some comments about that.

> * can we avoid `conda run` and just use `PATH` to run the executables (this'll be faster and is more consistent with the current `language`s)?

This will work with most, but not all executables. `conda` allows to set custom environment variables on environment activation. They could be required for some tools.

> * how does this interact with `language_version`? (this can be ignored for the first pass implementation)

We could force the conda version here but I don't think this would be helpful, especially if we were using a pre-installed conda.

> * how does this interact with `additional_dependencies`? (this can be ignored for the first pass implementation)

These can just be passed as additional arguments to the `conda create` call.

> * should we bootstrap `conda` for systems where it isn't installed (this can be ignored for the first pass implementation and just depend on system-installed `conda`)

`miniconda` normally asks you to accept a license on installation. I'm not sure whether it is ok to skip this when installing conda in the background.

> The name in the environment.yml file can be overridden by passing an env name to the conda env create command, so don't let that block you from using it.

@msarahan Which env var would that be? There is not mention of it on either https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-from-an-environment-yml-file nor https://docs.conda.io/projects/conda/en/latest/user-guide/concepts/environments.html
Specify the env name as a CLI flag (-n or -p):

```
conda env create -h
usage: conda-env create [-h] [-f FILE] [-n ENVIRONMENT | -p PATH] [--force]
                        [--json] [-v] [-q]
                        [remote_definition]

Create an environment based on an environment file

Options:

positional arguments:
  remote_definition     remote environment definition / IPython notebook

optional arguments:
  -h, --help            Show this help message and exit.
  -f FILE, --file FILE  environment definition file (default: environment.yml)
  --force               force creation of environment (removing a previously
                        existing environment of the same name).

Target Environment Specification:
  -n ENVIRONMENT, --name ENVIRONMENT
                        Name of environment.
  -p PATH, --prefix PATH
                        Full path to environment location (i.e. prefix).

Output, Prompt, and Flow Control Options:
  --json                Report all output as json. Suitable for using conda
                        programmatically.
  -v, --verbose         Use once for info, twice for debug, three times for
                        trace.
  -q, --quiet           Do not display progress bar.

examples:
    conda env create
    conda env create -n name
    conda env create vader/deathstar
    conda env create -f=/path/to/environment.yml
    conda env create -f=/path/to/requirements.txt -n deathstar
    conda env create -f=/path/to/requirements.txt -p /home/user/software/deathstar
```

Resolving patch:
diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py
--- a/pre_commit/languages/all.py
+++ b/pre_commit/languages/all.py
@@ -1,5 +1,6 @@
 from __future__ import unicode_literals
 
+from pre_commit.languages import conda
 from pre_commit.languages import docker
 from pre_commit.languages import docker_image
 from pre_commit.languages import fail
@@ -52,6 +53,7 @@
 #     """
 
 languages = {
+    'conda': conda,
     'docker': docker,
     'docker_image': docker_image,
     'fail': fail,
diff --git a/pre_commit/languages/conda.py b/pre_commit/languages/conda.py
new file mode 100644
--- /dev/null
+++ b/pre_commit/languages/conda.py
@@ -0,0 +1,66 @@
+import contextlib
+import os
+
+from pre_commit.envcontext import envcontext
+from pre_commit.envcontext import UNSET
+from pre_commit.envcontext import Var
+from pre_commit.languages import helpers
+from pre_commit.util import clean_path_on_failure
+from pre_commit.util import cmd_output_b
+
+ENVIRONMENT_DIR = 'conda'
+get_default_version = helpers.basic_get_default_version
+healthy = helpers.basic_healthy
+
+
+def get_env_patch(env):
+    # On non-windows systems executable live in $CONDA_PREFIX/bin, on Windows
+    # they can be in $CONDA_PREFIX/bin, $CONDA_PREFIX/Library/bin,
+    # $CONDA_PREFIX/Scripts and $CONDA_PREFIX. Whereas the latter only
+    # seems to be used for python.exe.
+    path = (os.path.join(env, 'bin'), os.pathsep, Var('PATH'))
+    if os.name == 'nt':  # pragma: no cover (platform specific)
+        path = (env, os.pathsep) + path
+        path = (os.path.join(env, 'Scripts'), os.pathsep) + path
+        path = (os.path.join(env, 'Library', 'bin'), os.pathsep) + path
+
+    return (
+        ('PYTHONHOME', UNSET),
+        ('VIRTUAL_ENV', UNSET),
+        ('CONDA_PREFIX', env),
+        ('PATH', path),
+    )
+
+
+@contextlib.contextmanager
+def in_env(prefix, language_version):
+    directory = helpers.environment_dir(ENVIRONMENT_DIR, language_version)
+    envdir = prefix.path(directory)
+    with envcontext(get_env_patch(envdir)):
+        yield
+
+
+def install_environment(prefix, version, additional_dependencies):
+    helpers.assert_version_default('conda', version)
+    directory = helpers.environment_dir(ENVIRONMENT_DIR, version)
+
+    env_dir = prefix.path(directory)
+    with clean_path_on_failure(env_dir):
+        cmd_output_b(
+            'conda', 'env', 'create', '-p', env_dir, '--file',
+            'environment.yml', cwd=prefix.prefix_dir,
+        )
+        if additional_dependencies:
+            cmd_output_b(
+                'conda', 'install', '-p', env_dir, *additional_dependencies,
+                cwd=prefix.prefix_dir
+            )
+
+
+def run_hook(hook, file_args, color):
+    # TODO: Some rare commands need to be run using `conda run` but mostly we
+    #       can run them withot which is much quicker and produces a better
+    #       output.
+    # cmd = ('conda', 'run', '-p', env_dir) + hook.cmd
+    with in_env(hook.prefix, hook.language_version):
+        return helpers.run_xargs(hook, hook.cmd, file_args, color=color)
diff --git a/pre_commit/store.py b/pre_commit/store.py
--- a/pre_commit/store.py
+++ b/pre_commit/store.py
@@ -173,7 +173,7 @@ def _git_cmd(*args):
 
     LOCAL_RESOURCES = (
         'Cargo.toml', 'main.go', 'main.rs', '.npmignore', 'package.json',
-        'pre_commit_dummy_package.gemspec', 'setup.py',
+        'pre_commit_dummy_package.gemspec', 'setup.py', 'environment.yml',
     )
 
     def make_local(self, deps):


Test patch:
diff --git a/testing/resources/conda_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/conda_hooks_repo/.pre-commit-hooks.yaml
new file mode 100644
--- /dev/null
+++ b/testing/resources/conda_hooks_repo/.pre-commit-hooks.yaml
@@ -0,0 +1,10 @@
+-   id: sys-exec
+    name: sys-exec
+    entry: python -c 'import os; import sys; print(sys.executable.split(os.path.sep)[-2]) if os.name == "nt" else print(sys.executable.split(os.path.sep)[-3])'
+    language: conda
+    files: \.py$
+-   id: additional-deps
+    name: additional-deps
+    entry: python
+    language: conda
+    files: \.py$
diff --git a/testing/resources/conda_hooks_repo/environment.yml b/testing/resources/conda_hooks_repo/environment.yml
new file mode 100644
--- /dev/null
+++ b/testing/resources/conda_hooks_repo/environment.yml
@@ -0,0 +1,6 @@
+channels:
+  - conda-forge
+  - defaults
+dependencies:
+  - python
+  - pip
diff --git a/tests/repository_test.py b/tests/repository_test.py
--- a/tests/repository_test.py
+++ b/tests/repository_test.py
@@ -79,6 +79,46 @@ def _test_hook_repo(
     assert _norm_out(out) == expected
 
 
+def test_conda_hook(tempdir_factory, store):
+    _test_hook_repo(
+        tempdir_factory, store, 'conda_hooks_repo',
+        'sys-exec', [os.devnull],
+        b'conda-default\n',
+    )
+
+
+def test_conda_with_additional_dependencies_hook(tempdir_factory, store):
+    _test_hook_repo(
+        tempdir_factory, store, 'conda_hooks_repo',
+        'additional-deps', [os.devnull],
+        b'OK\n',
+        config_kwargs={
+            'hooks': [{
+                'id': 'additional-deps',
+                'args': ['-c', 'import mccabe; print("OK")'],
+                'additional_dependencies': ['mccabe'],
+            }],
+        },
+    )
+
+
+def test_local_conda_additional_dependencies(store):
+    config = {
+        'repo': 'local',
+        'hooks': [{
+            'id': 'local-conda',
+            'name': 'local-conda',
+            'entry': 'python',
+            'language': 'conda',
+            'args': ['-c', 'import mccabe; print("OK")'],
+            'additional_dependencies': ['mccabe'],
+        }],
+    }
+    ret, out = _get_hook(config, store, 'local-conda').run((), color=False)
+    assert ret == 0
+    assert _norm_out(out) == b'OK\n'
+
+
 def test_python_hook(tempdir_factory, store):
     _test_hook_repo(
         tempdir_factory, store, 'python_hooks_repo',