Repository: pypa/pipenv

Issue:
Detecting virtualenv location in windows is unreliable when using powershell.
PS C:\Users\N N> cd work
PS C:\Users\N N\work> pipenv --venv
No virtualenv has been created for this project yet!
PS C:\Users\N N\work> cd ..
PS C:\Users\N N> cd .\Work\
PS C:\Users\N N\Work> pipenv --venv
C:\Users\N N\.virtualenvs\Work-DPz4PjCb
PS C:\Users\N N\Work>

Seems to work fine when using cmd.exe as the shell. Not a big thing, but slightly annoying.



Discussion:
@mteiste thanks for the report, this may be fixed with #1922 when merged, but not sure

Resolving patch:
diff --git a/pipenv/project.py b/pipenv/project.py
--- a/pipenv/project.py
+++ b/pipenv/project.py
@@ -14,6 +14,11 @@
 import pipfile.api
 import toml
 
+try:
+    import pathlib
+except ImportError:
+    import pathlib2 as pathlib
+
 from pip9 import ConfigOptionParser
 from .cmdparse import Script
 from .utils import (
@@ -41,12 +46,19 @@
     PIPENV_PYTHON,
 )
 
+
+def _normalized(p):
+    if p is None:
+        return None
+    return normalize_drive(str(pathlib.Path(p).resolve()))
+
+
 if PIPENV_PIPFILE:
     if not os.path.isfile(PIPENV_PIPFILE):
         raise RuntimeError('Given PIPENV_PIPFILE is not found!')
 
     else:
-        PIPENV_PIPFILE = normalize_drive(os.path.abspath(PIPENV_PIPFILE))
+        PIPENV_PIPFILE = _normalized(PIPENV_PIPFILE)
 # (path, file contents) => TOMLFile
 # keeps track of pipfiles that we've seen so we do not need to re-parse 'em
 _pipfile_cache = {}
@@ -203,8 +215,16 @@ def virtualenv_exists(self):
 
         return False
 
-    @property
-    def virtualenv_name(self):
+    @classmethod
+    def _get_virtualenv_location(cls, name):
+        from pipenv.patched.pew.pew import get_workon_home
+        venv = get_workon_home() / name
+        if not venv.exists():
+            return ''
+        return '{0}'.format(venv)
+
+    @classmethod
+    def _sanitize(cls, name):
         # Replace dangerous characters into '_'. The length of the sanitized
         # project name is limited as 42 because of the limit of linux kernel
         #
@@ -217,17 +237,50 @@ def virtualenv_name(self):
         #   https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html
         #   http://www.tldp.org/LDP/abs/html/special-chars.html#FIELDREF
         #   https://github.com/torvalds/linux/blob/2bfe01ef/include/uapi/linux/binfmts.h#L18
-        sanitized = re.sub(r'[ $`!*@"\\\r\n\t]', '_', self.name)[0:42]
-        # Hash the full path of the pipfile
-        hash = hashlib.sha256(self.pipfile_location.encode()).digest()[:6]
-        encoded_hash = base64.urlsafe_b64encode(hash).decode()
+        return re.sub(r'[ $`!*@"\\\r\n\t]', '_', name)[0:42]
+
+    def _get_virtualenv_hash(self, name):
+        """Get the name of the virtualenv adjusted for windows if needed
+
+        Returns (name, encoded_hash)
+        """
+        def get_name(name, location):
+            name = self._sanitize(name)
+            hash = hashlib.sha256(location.encode()).digest()[:6]
+            encoded_hash = base64.urlsafe_b64encode(hash).decode()
+            return name, encoded_hash[:8]
+
+        clean_name, encoded_hash = get_name(name, self.pipfile_location)
+        venv_name = '{0}-{1}'.format(clean_name, encoded_hash)
+
+        # This should work most of the time, for non-WIndows, in-project venv,
+        # or "proper" path casing (on Windows).
+        if (os.name != 'nt' or
+                PIPENV_VENV_IN_PROJECT or
+                self._get_virtualenv_location(venv_name)):
+            return clean_name, encoded_hash
+
+        # Check for different capitalization of the same project.
+        from pipenv.patched.pew.pew import lsenvs
+        for env in lsenvs():
+            env_name = env[:-9]
+            if not (env[-9] != '-' and
+                    env[-8:].isalpha() and
+                    env_name.lower() != name.lower()):
+                continue
+            return get_name(env_name, self.pipfile_location.replace(name, env_name))
+
+        # Use the default if no matching env exists.
+        return clean_name, encoded_hash
+
+
+    @property
+    def virtualenv_name(self):
+        sanitized, encoded_hash = self._get_virtualenv_hash(self.name)
+        suffix = '-{0}'.format(PIPENV_PYTHON) if PIPENV_PYTHON else ''
         # If the pipfile was located at '/home/user/MY_PROJECT/Pipfile',
         # the name of its virtualenv will be 'my-project-wyUfYPqE'
-        if PIPENV_PYTHON:
-            return sanitized + '-' + encoded_hash + '-' + PIPENV_PYTHON
-
-        else:
-            return sanitized + '-' + encoded_hash
+        return sanitized + '-' + encoded_hash + suffix
 
     @property
     def virtualenv_location(self):
@@ -243,13 +296,7 @@ def virtualenv_location(self):
 
         # Default mode.
         if not venv_in_project:
-            c = delegator.run(
-                '{0} -m pipenv.pew dir "{1}"'.format(
-                    escape_grouped_arguments(sys.executable),
-                    self.virtualenv_name,
-                )
-            )
-            loc = c.out.strip()
+            loc = self._get_virtualenv_location(self.virtualenv_name)
         # The user wants the virtualenv in the project.
         else:
             loc = os.sep.join(
@@ -304,7 +351,7 @@ def pipfile_location(self):
                 loc = pipfile.Pipfile.find(max_depth=PIPENV_MAX_DEPTH)
             except RuntimeError:
                 loc = None
-            self._pipfile_location = normalize_drive(loc)
+            self._pipfile_location = _normalized(loc)
         return self._pipfile_location
 
     @property


Test patch:
diff --git a/tests/test_pipenv.py b/tests/test_pipenv.py
--- a/tests/test_pipenv.py
+++ b/tests/test_pipenv.py
@@ -1356,3 +1356,19 @@ def test_resolver_unique_markers(self, pypi):
             yarl = p.lockfile['default']['yarl']
             assert 'markers' in yarl
             assert yarl['markers'] == "python_version in '3.4, 3.5, 3.6'"
+
+    @pytest.mark.project
+    @pytest.mark.skipif(os.name != 'nt', reason='Test project matching for case changes on win')
+    def test_case_changes_windows(self, pypi):
+        with PipenvInstance(pypi=pypi, chdir=True) as p:
+            c = p.pipenv('install pytz')
+            assert c.return_code == 0
+            virtualenv_location = Project().virtualenv_location
+            target = p.path.upper()
+            if target == p.path:
+                target = p.path.lower()
+            os.chdir('..')
+            os.chdir(target)
+            assert os.path.abspath(os.curdir) != p.path
+            venv = delegator.run('pipenv --venv').out
+            assert venv.strip().lower() == virtualenv_location.lower()