Repository: conda/conda

Issue:
conda env update fails with pip dependencies if Python version changes
**I'm submitting a...**
  - [x] bug report
  - [ ] feature request

**update:** only the root env appears to be affected

## Current Behavior

`conda env update -f environment.yml` fails if both of these conditions are met:

1. The Python version changes
2. There is at least one dependency to be installed with pip

The error looks like:

```
Unable to install package for pip.

Please double check and ensure you dependencies file has
the correct spelling.  You might also try installing the
conda-env-pip package to see if provides the required
installer.
```

This is the same error but not the same bug as #4985. pip is installed and specified as a dependency, but switching Python version in the same transaction results in conda failing to find pip.

### Steps to Reproduce

Create an environment.yml that contains Python and at least one pip dependency (doesn't matter if it's already installed or not):

```yaml
# environment.yml
dependencies:
  - python=3.5
  - pip
  - pip:
    - wheel
```

Setup a root env with a *different* Python:

```
conda install python=3.6 pip
```

and then try to update it with the environment file

```
conda env update -n root -f environment.yml
```


## Expected Behavior

`conda env update` should succeed.
<!-- What do you think should happen? -->


## Environment Information

Run in docker with [these files](https://gist.github.com/7d234cbcf69df2c7cd68523c9327db71).

<details open><summary><code>`conda info`</code></summary><p>
<!-- between the ticks below, paste the output of 'conda info' -->

```
     active environment : None
       user config file : /root/.condarc
 populated config files :
          conda version : 4.4.8
    conda-build version : not installed
         python version : 3.6.2.final.0
       base environment : /opt/conda  (writable)
           channel URLs : https://repo.continuum.io/pkgs/main/linux-64
                          https://repo.continuum.io/pkgs/main/noarch
                          https://repo.continuum.io/pkgs/free/linux-64
                          https://repo.continuum.io/pkgs/free/noarch
                          https://repo.continuum.io/pkgs/r/linux-64
                          https://repo.continuum.io/pkgs/r/noarch
                          https://repo.continuum.io/pkgs/pro/linux-64
                          https://repo.continuum.io/pkgs/pro/noarch
          package cache : /opt/conda/pkgs
                          /root/.conda/pkgs
       envs directories : /opt/conda/envs
                          /root/.conda/envs
               platform : linux-64
             user-agent : conda/4.4.8 requests/2.18.4 CPython/3.6.2 Linux/4.9.75-linuxkit-aufs debian/8 glibc/2.19
                UID:GID : 0:0
             netrc file : None
           offline mode : False
```
</p></details>


conda env update fails with pip dependencies if Python version changes
**I'm submitting a...**
  - [x] bug report
  - [ ] feature request

**update:** only the root env appears to be affected

## Current Behavior

`conda env update -f environment.yml` fails if both of these conditions are met:

1. The Python version changes
2. There is at least one dependency to be installed with pip

The error looks like:

```
Unable to install package for pip.

Please double check and ensure you dependencies file has
the correct spelling.  You might also try installing the
conda-env-pip package to see if provides the required
installer.
```

This is the same error but not the same bug as #4985. pip is installed and specified as a dependency, but switching Python version in the same transaction results in conda failing to find pip.

### Steps to Reproduce

Create an environment.yml that contains Python and at least one pip dependency (doesn't matter if it's already installed or not):

```yaml
# environment.yml
dependencies:
  - python=3.5
  - pip
  - pip:
    - wheel
```

Setup a root env with a *different* Python:

```
conda install python=3.6 pip
```

and then try to update it with the environment file

```
conda env update -n root -f environment.yml
```


## Expected Behavior

`conda env update` should succeed.
<!-- What do you think should happen? -->


## Environment Information

Run in docker with [these files](https://gist.github.com/7d234cbcf69df2c7cd68523c9327db71).

<details open><summary><code>`conda info`</code></summary><p>
<!-- between the ticks below, paste the output of 'conda info' -->

```
     active environment : None
       user config file : /root/.condarc
 populated config files :
          conda version : 4.4.8
    conda-build version : not installed
         python version : 3.6.2.final.0
       base environment : /opt/conda  (writable)
           channel URLs : https://repo.continuum.io/pkgs/main/linux-64
                          https://repo.continuum.io/pkgs/main/noarch
                          https://repo.continuum.io/pkgs/free/linux-64
                          https://repo.continuum.io/pkgs/free/noarch
                          https://repo.continuum.io/pkgs/r/linux-64
                          https://repo.continuum.io/pkgs/r/noarch
                          https://repo.continuum.io/pkgs/pro/linux-64
                          https://repo.continuum.io/pkgs/pro/noarch
          package cache : /opt/conda/pkgs
                          /root/.conda/pkgs
       envs directories : /opt/conda/envs
                          /root/.conda/envs
               platform : linux-64
             user-agent : conda/4.4.8 requests/2.18.4 CPython/3.6.2 Linux/4.9.75-linuxkit-aufs debian/8 glibc/2.19
                UID:GID : 0:0
             netrc file : None
           offline mode : False
```
</p></details>




Discussion:
Inspecting the code a bit, I assume that the issue is that the imported `conda_env` has been deleted and the `get_installer` call imports `conda_env.installers.pip` *after* the conda installer has removed the files (by switching from 3.5/site-packages to 3.6/site-packages).  I *think* if the installer were imported before applying the transaction, it would be okay. Unfortunately, this is very difficult to test because it only affects the root environment, and cannot affect dev-installs (since imported conda_env is not removed during the upgrade in those cases).
Inspecting the code a bit, I assume that the issue is that the imported `conda_env` has been deleted and the `get_installer` call imports `conda_env.installers.pip` *after* the conda installer has removed the files (by switching from 3.5/site-packages to 3.6/site-packages).  I *think* if the installer were imported before applying the transaction, it would be okay. Unfortunately, this is very difficult to test because it only affects the root environment, and cannot affect dev-installs (since imported conda_env is not removed during the upgrade in those cases).

Resolving patch:
diff --git a/conda_env/cli/main_update.py b/conda_env/cli/main_update.py
--- a/conda_env/cli/main_update.py
+++ b/conda_env/cli/main_update.py
@@ -101,11 +101,15 @@ def execute(args, parser):
     # common.ensure_override_channels_requires_channel(args)
     # channel_urls = args.channel or ()
 
-    for installer_type, specs in env.dependencies.items():
+    # create installers before running any of them
+    # to avoid failure to import after the file being deleted
+    # e.g. due to conda_env being upgraded or Python version switched.
+    installers = {}
+
+    for installer_type in env.dependencies:
         try:
-            installer = get_installer(installer_type)
-            installer.install(prefix, specs, args, env)
-        except InvalidInstaller:
+            installers[installer_type] = get_installer(installer_type)
+        except InvalidInstaller as e:
             sys.stderr.write(textwrap.dedent("""
                 Unable to install package for {0}.
 
@@ -117,5 +121,9 @@ def execute(args, parser):
             )
             return -1
 
+    for installer_type, specs in env.dependencies.items():
+        installer = installers[installer_type]
+        installer.install(prefix, specs, args, env)
+
     touch_nonadmin(prefix)
     cli_install.print_activate(args.name if args.name else prefix)


Test patch: