Repository: pypa/pipenv
Issue: PIPENV_IGNORE_VIRTUALENVS and pipenv shell

On my Windows 10 machine, I have PIPENV_IGNORE_VIRTUALENVS set to 1 and most pipenv commands seem to work using the .venv correctly... with the exception of `pipenv shell`

    Adam@ADAM-DESKTOP C:\\Users\\Adam\\projects\\project1
    > pipenv install
    Creating a virtualenv for this project…
    Using base prefix 'c:\\\\python36'
    New python executable in C:\\Users\\Adam\\projects\\project1\\.venv\\Scripts\\python.exe
    Installing setuptools, pip, wheel...done.
    
    Virtualenv location: C:\\Users\\Adam\\projects\\project1\\.venv
    Creating a Pipfile for this project…
    Pipfile.lock not found, creating…
    Locking [dev-packages] dependencies…
    Locking [packages] dependencies…
    Updated Pipfile.lock!
    Installing dependencies from Pipfile.lock…
      ================================ 0/0 — 00:00:00
    To activate this project's virtualenv, run the following:
     $ pipenv shell
    
    Adam@ADAM-DESKTOP C:\\Users\\Adam\\projects\\project1
    > pipenv --venv
    C:\\Users\\Adam\\projects\\project1\\.venv
    
    Adam@ADAM-DESKTOP C:\\Users\\Adam\\projects\\project1
    > pipenv shell
    ERROR: Environment 'project1-z9NXaua1' does not exist. Create it with 'pew new project1-z9NXaua1'.

Changed files:
tests/test_pipenv.py, pipenv/cli.py, pipenv/utils.py

diff --git a/pipenv/cli.py b/pipenv/cli.py
index 031f373e..f8b07cd6 100644
--- a/pipenv/cli.py
+++ b/pipenv/cli.py
@@ -25,19 +25,17 @@ import pipdeptree
 import requirements
 import semver
 import flake8.main.cli
-
 from pipreqs import pipreqs
 from blindspin import spinner
 from urllib3.exceptions import InsecureRequestWarning
 from pip.req.req_file import parse_requirements
 from click_didyoumean import DYMCommandCollection
-
 from .project import Project
 from .utils import (
     convert_deps_from_pip, convert_deps_to_pip, is_required_version,
     proper_case, pep423_name, split_vcs, resolve_deps, shellquote, is_vcs,
     python_version, suggest_package, find_windows_executable, is_file,
-    prepare_pip_source_args
+    prepare_pip_source_args, temp_environ
 )
 from .__version__ import __version__
 from . import pep508checker, progress
@@ -2012,29 +2010,43 @@ def do_shell(three=None, python=False, fancy=False, shell_args=None):
 
     # Standard (properly configured shell) mode:
     else:
+        if PIPENV_VENV_IN_PROJECT:
+            # use .venv as the target virtualenv name
+            workon_name = '.venv'
+        else:
+            workon_name = project.virtualenv_name
+
         cmd = 'pew'
-        args = ["workon", project.virtualenv_name]
+        args = ["workon", workon_name]
 
     # Grab current terminal dimensions to replace the hardcoded default
     # dimensions of pexpect
     terminal_dimensions = get_terminal_size()
 
     try:
-        c = pexpect.spawn(
-            cmd,
-            args,
-            dimensions=(
-                terminal_dimensions.lines,
-                terminal_dimensions.columns
+        with temp_environ():
+            if PIPENV_VENV_IN_PROJECT:
+                os.environ['WORKON_HOME'] = project.project_directory
+
+            c = pexpect.spawn(
+                cmd,
+                args,
+                dimensions=(
+                    terminal_dimensions.lines,
+                    terminal_dimensions.columns
+                )
             )
-        )
 
     # Windows!
     except AttributeError:
         import subprocess
-        p = subprocess.Popen([cmd] + list(args), shell=True, universal_newlines=True)
-        p.communicate()
-        sys.exit(p.returncode)
+        # Tell pew to use the project directory as its workon_home
+        with temp_environ():
+            if PIPENV_VENV_IN_PROJECT:
+                os.environ['WORKON_HOME'] = project.project_directory
+            p = subprocess.Popen([cmd] + list(args), shell=True, universal_newlines=True)
+            p.communicate()
+            sys.exit(p.returncode)
 
     # Activate the virtualenv if in compatibility mode.
     if compat:
diff --git a/pipenv/utils.py b/pipenv/utils.py
index 3fea5fab..3008a728 100644
--- a/pipenv/utils.py
+++ b/pipenv/utils.py
@@ -22,6 +22,7 @@ try:
 except ImportError:
     from urlparse import urlparse
 
+from contextlib import contextmanager
 from piptools.resolver import Resolver
 from piptools.repositories.pypi import PyPIRepository
 from piptools.scripts.compile import get_pip_command
@@ -913,3 +914,16 @@ def find_requirements(max_depth=3):
                 if os.path.isfile(r):
                     return r
     raise RuntimeError('No requirements.txt found!')
+
+
+# Borrowed from pew to avoid importing pew which imports psutil
+# See https://github.com/berdario/pew/blob/master/pew/_utils.py#L82
+@contextmanager
+def temp_environ():
+    """Allow the ability to set os.environ temporarily"""
+    environ = dict(os.environ)
+    try:
+        yield
+    finally:
+        os.environ.clear()
+        os.environ.update(environ)
diff --git a/tests/test_pipenv.py b/tests/test_pipenv.py
index c2355e5c..e56b89ac 100644
--- a/tests/test_pipenv.py
+++ b/tests/test_pipenv.py
@@ -6,6 +6,7 @@ import json
 import pytest
 
 from pipenv.cli import activate_virtualenv
+from pipenv.utils import temp_environ, get_windows_path
 from pipenv.vendor import toml
 from pipenv.vendor import delegator
 from pipenv.project import Project
@@ -271,33 +272,32 @@ class TestPipenv:
     @pytest.mark.run
     @pytest.mark.install
     def test_multiprocess_bug_and_install(self):
-        os.environ['PIPENV_MAX_SUBPROCESS'] = '2'
+        with temp_environ():
+            os.environ['PIPENV_MAX_SUBPROCESS'] = '2'
 
-        with PipenvInstance() as p:
-            with open(p.pipfile_path, 'w') as f:
-                contents = """
+            with PipenvInstance() as p:
+                with open(p.pipfile_path, 'w') as f:
+                    contents = """
 [packages]
 requests = "*"
 records = "*"
 tpfd = "*"
-                """.strip()
-                f.write(contents)
+                    """.strip()
+                    f.write(contents)
 
-            c = p.pipenv('install')
-            assert c.return_code == 0
-
-            assert 'requests' in p.lockfile['default']
-            assert 'idna' in p.lockfile['default']
-            assert 'urllib3' in p.lockfile['default']
-            assert 'certifi' in p.lockfile['default']
-            assert 'records' in p.lockfile['default']
-            assert 'tpfd' in p.lockfile['default']
-            assert 'parse' in p.lockfile['default']
+                c = p.pipenv('install')
+                assert c.return_code == 0
 
-            c = p.pipenv('run python -c "import requests; import idna; import certifi; import records; import tpfd; import parse;"')
-            assert c.return_code == 0
+                assert 'requests' in p.lockfile['default']
+                assert 'idna' in p.lockfile['default']
+                assert 'urllib3' in p.lockfile['default']
+                assert 'certifi' in p.lockfile['default']
+                assert 'records' in p.lockfile['default']
+                assert 'tpfd' in p.lockfile['default']
+                assert 'parse' in p.lockfile['default']
 
-            del os.environ['PIPENV_MAX_SUBPROCESS']
+                c = p.pipenv('run python -c "import requests; import idna; import certifi; import records; import tpfd; import parse;"')
+                assert c.return_code == 0
 
     @pytest.mark.sequential
     @pytest.mark.install
@@ -445,14 +445,60 @@ requests = {version = "*"}
     @pytest.mark.dotvenv
     def test_venv_in_project(self):
 
-        os.environ['PIPENV_VENV_IN_PROJECT'] = '1'
-        with PipenvInstance() as p:
-            c = p.pipenv('install requests')
-            assert c.return_code == 0
+        with temp_environ():
+            os.environ['PIPENV_VENV_IN_PROJECT'] = '1'
+            with PipenvInstance() as p:
+                c = p.pipenv('install requests')
+                assert c.return_code == 0
 
-            assert p.path in p.pipenv('--venv').out
+                assert p.path in p.pipenv('--venv').out
+
+    @pytest.mark.dotvenv
+    @pytest.mark.install
+    @pytest.mark.complex
+    @pytest.mark.shell
+    @pytest.mark.windows
+    @pytest.mark.pew
+    def test_shell_nested_venv_in_project(self):
+        import subprocess
+        with temp_environ():
+            os.environ['PIPENV_VENV_IN_PROJECT'] = '1'
+            os.environ['PIPENV_IGNORE_VIRTUALENVS'] = '1'
+            os.environ['PIPENV_SHELL_COMPAT'] = '1'
+            with PipenvInstance(chdir=True) as p:
+                # Signal to pew to look in the project directory for the environment
+                os.environ['WORKON_HOME'] = p.path
+                project = Project()
+                c = p.pipenv('install requests')
+                assert c.return_code == 0
+                assert 'requests' in p.pipfile['packages']
+                assert 'requests' in p.lockfile['default']
+                # Check that .venv now shows in pew's managed list
+                pew_list = delegator.run('pew ls')
+                assert '.venv' in pew_list.out
+                # Check for the venv directory 
+                c = delegator.run('pew dir .venv')
+                # Compare pew's virtualenv path to what we expect
+                venv_path = get_windows_path(project.project_directory, '.venv')
+                # os.path.normpath will normalize slashes
+                assert venv_path == os.path.normpath(c.out.strip())
+                # Have pew run 'pip freeze' in the virtualenv
+                # This is functionally the same as spawning a subshell
+                # If we can do this we can theoretically amke a subshell
+                # This test doesn't work on *nix
+                if os.name == 'nt':
+                    args = ['pew', 'in', '.venv', 'pip', 'freeze']
+                    process = subprocess.Popen(
+                        args,
+                        shell=True,
+                        universal_newlines=True,
+                        stdin=subprocess.PIPE,
+                        stdout=subprocess.PIPE,
+                        stderr=subprocess.PIPE
+                    )
+                    out, _ = process.communicate()
+                    assert any(req.startswith('requests') for req in out.splitlines()) is True
 
-        del os.environ['PIPENV_VENV_IN_PROJECT']
 
     @pytest.mark.run
     @pytest.mark.dotenv