Repository: conda/conda

Issue:
Invalid requirement while trying to use pip options
Hi!

I have in my pip section inside envrionment.yaml file this line
```- rep --install-option='--no-deps'```

while I am trying to update my environment I am getting this error

```Invalid requirement: 'rep --install-option='--no-deps''```

if I do pip -r requirements.txt and I have that line as it is in requirements.txt it works.



Discussion:

Resolving patch:
diff --git a/conda/egg_info.py b/conda/egg_info.py
--- a/conda/egg_info.py
+++ b/conda/egg_info.py
@@ -30,6 +30,10 @@ def get_site_packages_dir(installed_pkgs):
 
 def get_egg_info_files(sp_dir):
     for fn in os.listdir(sp_dir):
+        if fn.endswith('.egg-link'):
+            with open(join(sp_dir, fn), 'r') as reader:
+                for egg in get_egg_info_files(reader.readline().strip()):
+                    yield egg
         if not fn.endswith(('.egg', '.egg-info', '.dist-info')):
             continue
         path = join(sp_dir, fn)
diff --git a/conda_env/installers/pip.py b/conda_env/installers/pip.py
--- a/conda_env/installers/pip.py
+++ b/conda_env/installers/pip.py
@@ -1,13 +1,56 @@
 from __future__ import absolute_import
+
+import os
+import os.path as op
 import subprocess
+import tempfile
 from conda_env.pip_util import pip_args
 from conda.exceptions import CondaValueError
 
 
-def install(prefix, specs, args, env, prune=False):
-    pip_cmd = pip_args(prefix) + ['install', ] + specs
-    process = subprocess.Popen(pip_cmd, universal_newlines=True)
-    process.communicate()
+def _pip_install_via_requirements(prefix, specs, args, *_):
+    """
+    Installs the pip dependencies in specs using a temporary pip requirements file.
+
+    Args
+    ----
+    prefix: string
+      The path to the python and pip executables.
+
+    specs: iterable of strings
+      Each element should be a valid pip dependency.
+      See: https://pip.pypa.io/en/stable/user_guide/#requirements-files
+           https://pip.pypa.io/en/stable/reference/pip_install/#requirements-file-format
+    """
+    try:
+        pip_workdir = op.dirname(op.abspath(args.file))
+    except AttributeError:
+        pip_workdir = None
+    requirements = None
+    try:
+        # Generate the temporary requirements file
+        requirements = tempfile.NamedTemporaryFile(mode='w',
+                                                   prefix='condaenv.',
+                                                   suffix='.requirements.txt',
+                                                   dir=pip_workdir,
+                                                   delete=False)
+        requirements.write('\n'.join(specs))
+        requirements.close()
+        # pip command line...
+        pip_cmd = pip_args(prefix) + ['install', '-r', requirements.name]
+        # ...run it
+        process = subprocess.Popen(pip_cmd,
+                                   cwd=pip_workdir,
+                                   universal_newlines=True)
+        process.communicate()
+        if process.returncode != 0:
+            raise CondaValueError("pip returned an error")
+    finally:
+        # Win/Appveyor does not like it if we use context manager + delete=True.
+        # So we delete the temporary file in a finally block.
+        if requirements is not None and op.isfile(requirements.name):
+            os.remove(requirements.name)
+
 
-    if process.returncode != 0:
-        raise CondaValueError("pip returned an error.")
+# Conform to Installers API
+install = _pip_install_via_requirements


Test patch:
diff --git a/tests/conda_env/installers/test_pip.py b/tests/conda_env/installers/test_pip.py
--- a/tests/conda_env/installers/test_pip.py
+++ b/tests/conda_env/installers/test_pip.py
@@ -4,21 +4,40 @@
 except ImportError:
     import mock
 
+import os
 from conda_env.installers import pip
 from conda.exceptions import CondaValueError
 
+
 class PipInstallerTest(unittest.TestCase):
     def test_straight_install(self):
-        with mock.patch.object(pip.subprocess, 'Popen') as popen:
-            popen.return_value.returncode = 0
-            with mock.patch.object(pip, 'pip_args') as pip_args:
-                pip_args.return_value = ['pip']
 
-                pip.install('/some/prefix', ['foo'], '', '')
+        # To check that the correct file would be written
+        written_deps = []
+
+        def log_write(text):
+            written_deps.append(text)
+            return mock.DEFAULT
 
-                popen.assert_called_with(['pip', 'install', 'foo'],
-                                         universal_newlines=True)
-                self.assertEqual(1, popen.return_value.communicate.call_count)
+        with mock.patch.object(pip.subprocess, 'Popen') as mock_popen, \
+                mock.patch.object(pip, 'pip_args') as mock_pip_args, \
+                mock.patch('tempfile.NamedTemporaryFile', mock.mock_open()) as mock_namedtemp:
+            # Mock
+            mock_popen.return_value.returncode = 0
+            mock_pip_args.return_value = ['pip']
+            mock_namedtemp.return_value.write.side_effect = log_write
+            mock_namedtemp.return_value.name = 'tmp-file'
+            args = mock.Mock()
+            root_dir = '/whatever' if os.name != 'nt' else 'C:\\whatever'
+            args.file = os.path.join(root_dir, 'environment.yml')
+            # Run
+            pip.install('/some/prefix', ['foo', '-e ./bar'], args)
+            # Check expectations
+            mock_popen.assert_called_with(['pip', 'install', '-r', 'tmp-file'],
+                                          cwd=root_dir,
+                                          universal_newlines=True)
+            self.assertEqual(1, mock_popen.return_value.communicate.call_count)
+            self.assertEqual(written_deps, ['foo\n-e ./bar'])
 
     def test_stops_on_exception(self):
         with mock.patch.object(pip.subprocess, 'Popen') as popen:
@@ -28,4 +47,4 @@ def test_stops_on_exception(self):
                 pip_args.return_value = ['pip']
 
                 self.assertRaises(CondaValueError, pip.install,
-                                  '/some/prefix', ['foo'], '', '')
+                                  '/some/prefix', ['foo'], None)
diff --git a/tests/conda_env/support/advanced-pip/.gitignore b/tests/conda_env/support/advanced-pip/.gitignore
new file mode 100644
--- /dev/null
+++ b/tests/conda_env/support/advanced-pip/.gitignore
@@ -0,0 +1 @@
+src
diff --git a/tests/conda_env/support/advanced-pip/another-project-requirements.txt b/tests/conda_env/support/advanced-pip/another-project-requirements.txt
new file mode 100644
--- /dev/null
+++ b/tests/conda_env/support/advanced-pip/another-project-requirements.txt
@@ -0,0 +1 @@
+six
diff --git a/tests/conda_env/support/advanced-pip/environment.yml b/tests/conda_env/support/advanced-pip/environment.yml
new file mode 100644
--- /dev/null
+++ b/tests/conda_env/support/advanced-pip/environment.yml
@@ -0,0 +1,29 @@
+name: advanced-pip-example
+
+dependencies:
+  - pip
+  - pip:
+
+    # Global options can be tweaked.
+    # For example, if you want to use a pypi mirror first:
+    - --index-url https://pypi.doubanio.com/simple
+    - --extra-index-url https://pypi.python.org/simple
+    # (check https://www.pypi-mirrors.org/)
+
+    # Current syntax still works
+    - xmltodict==0.10.2
+
+    # Install in editable mode.
+    # More natural than - "--editable=git+https://github.com/neithere/argh.git#egg=argh
+    - -e git+https://github.com/neithere/argh.git#egg=argh
+
+    # You could also specify a package in a directory.
+    # The directory can be relative to this environment file.
+    - -e ./module_to_install_in_editable_mode
+
+    # Use another requirements file.
+    # Note that here also we can use relative paths.
+    # pip will be run from the environment file directory, if provided.
+    - -r another-project-requirements.txt
+
+    # Anything else that pip requirement files allows should work seamlessly...
diff --git a/tests/conda_env/support/advanced-pip/module_to_install_in_editable_mode/setup.py b/tests/conda_env/support/advanced-pip/module_to_install_in_editable_mode/setup.py
new file mode 100644
--- /dev/null
+++ b/tests/conda_env/support/advanced-pip/module_to_install_in_editable_mode/setup.py
@@ -0,0 +1,6 @@
+from setuptools import setup
+
+setup(
+    name='module_to_install_in_editable_mode',
+    packages=[],
+)
diff --git a/tests/conda_env/test_create.py b/tests/conda_env/test_create.py
--- a/tests/conda_env/test_create.py
+++ b/tests/conda_env/test_create.py
@@ -2,10 +2,12 @@
 from __future__ import absolute_import, division, print_function
 
 from argparse import ArgumentParser
+from unittest import TestCase
 
 from conda.base.context import reset_context
 from conda.common.io import env_var
 
+from conda.egg_info import get_egg_info
 from conda.exports import text_type
 from contextlib import contextmanager
 from logging import getLogger, Handler
@@ -13,7 +15,6 @@
 from shlex import split
 from shutil import rmtree
 from tempfile import mkdtemp
-from unittest import TestCase
 from uuid import uuid4
 
 import pytest
@@ -60,7 +61,7 @@ class Commands:
 }
 
 
-def run_command(command, envs_dir, env_name, *arguments):
+def run_command(command, env_name, *arguments):
     p = ArgumentParser()
     sub_parsers = p.add_subparsers(metavar='command', dest='cmd')
     parser_config[command](sub_parsers)
@@ -81,8 +82,8 @@ def make_temp_envs_dir():
         rmtree(envs_dir, ignore_errors=True)
 
 
-def package_is_installed(prefix, dist, exact=False):
-    packages = list(linked(prefix))
+def package_is_installed(prefix, dist, exact=False, pip=False):
+    packages = list(get_egg_info(prefix) if pip else linked(prefix))
     if '::' not in text_type(dist):
         packages = [p.dist_name for p in packages]
     if exact:
@@ -90,8 +91,8 @@ def package_is_installed(prefix, dist, exact=False):
     return any(p.startswith(dist) for p in packages)
 
 
-def assert_package_is_installed(prefix, package, exact=False):
-    if not package_is_installed(prefix, package, exact):
+def assert_package_is_installed(prefix, package, exact=False, pip=False):
+    if not package_is_installed(prefix, package, exact, pip):
         print(list(linked(prefix)))
         raise AssertionError("package {0} is not in prefix".format(package))
 
@@ -112,10 +113,25 @@ def test_create_update(self):
                 prefix = join(envs_dir, env_name)
                 python_path = join(prefix, PYTHON_BINARY)
 
-                run_command(Commands.CREATE, envs_dir, env_name, utils.support_file('example/environment_pinned.yml'))
+                run_command(Commands.CREATE, env_name, utils.support_file('example/environment_pinned.yml'))
                 assert exists(python_path)
                 assert_package_is_installed(prefix, 'flask-0.9')
 
-                run_command(Commands.UPDATE, envs_dir, env_name, utils.support_file('example/environment_pinned_updated.yml'))
+                run_command(Commands.UPDATE, env_name, utils.support_file('example/environment_pinned_updated.yml'))
                 assert_package_is_installed(prefix, 'flask-0.10.1')
                 assert not package_is_installed(prefix, 'flask-0.9')
+
+    def test_create_advanced_pip(self):
+        with make_temp_envs_dir() as envs_dir:
+            with env_var('CONDA_ENVS_DIRS', envs_dir, reset_context):
+                env_name = str(uuid4())[:8]
+                prefix = join(envs_dir, env_name)
+                python_path = join(prefix, PYTHON_BINARY)
+
+                run_command(Commands.CREATE, env_name,
+                            utils.support_file('advanced-pip/environment.yml'))
+                assert exists(python_path)
+                assert_package_is_installed(prefix, 'argh', exact=False, pip=True)
+                assert_package_is_installed(prefix, 'module-to-install-in-editable-mode', exact=False, pip=True)
+                assert_package_is_installed(prefix, 'six', exact=False, pip=True)
+                assert_package_is_installed(prefix, 'xmltodict-0.10.2-<pip>', exact=True, pip=True)