From 9ab96dc5537a5c28b7ec5960a4cc964614f4ac2a Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Mon, 9 Apr 2018 21:45:22 -0400 Subject: [PATCH 1/3] Try to lowercase virtualenv locations - Fixes #1925 Signed-off-by: Dan Ryan --- pipenv/project.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/pipenv/project.py b/pipenv/project.py index 001652fe..7086a998 100644 --- a/pipenv/project.py +++ b/pipenv/project.py @@ -201,6 +201,11 @@ class Project(object): return False + def _get_virtualenv_location(cls, name): + """Get the path to a virtualenv from its name""" + venv = delegator.run('{0} -m pipenv.pew dir "{1}"'.format(escape_grouped_arguments(sys.executable), name)).out + return venv.strip() + @property def virtualenv_name(self): # Replace dangerous characters into '_'. The length of the sanitized @@ -219,6 +224,11 @@ class Project(object): # Hash the full path of the pipfile hash = hashlib.sha256(self.pipfile_location.encode()).digest()[:6] encoded_hash = base64.urlsafe_b64encode(hash).decode() + if os.name == 'nt' and not PIPENV_VENV_IN_PROJECT and sanitized.lower() != sanitized: + venv = self._get_virtualenv_location(sanitized) + lower_venv = self._get_virtualenv_location(sanitized.lower()) + if not venv.strip() and lower_venv.strip(): + sanitized = sanitized.lower() # If the pipfile was located at '/home/user/MY_PROJECT/Pipfile', # the name of its virtualenv will be 'my-project-wyUfYPqE' if PIPENV_PYTHON: @@ -241,13 +251,7 @@ class Project(object): # 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( From e3f8d4fded9d0f51493771355f3a82480043e3e0 Mon Sep 17 00:00:00 2001 From: Dan Ryan Date: Tue, 10 Apr 2018 00:21:07 -0400 Subject: [PATCH 2/3] Final changes and tests for case normalization - Fix windows venv case normalization Signed-off-by: Dan Ryan --- pipenv/project.py | 58 +++++++++++++++++++++++++++++--------------- tests/test_pipenv.py | 16 ++++++++++++ 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/pipenv/project.py b/pipenv/project.py index 7086a998..f9177d0a 100644 --- a/pipenv/project.py +++ b/pipenv/project.py @@ -11,6 +11,7 @@ import hashlib import contoml import delegator +from pipenv.vendor.first import first import pipfile import pipfile.api import toml @@ -201,13 +202,16 @@ class Project(object): return False + @classmethod def _get_virtualenv_location(cls, name): - """Get the path to a virtualenv from its name""" - venv = delegator.run('{0} -m pipenv.pew dir "{1}"'.format(escape_grouped_arguments(sys.executable), name)).out - return venv.strip() + from pipenv.patched.pew.pew import get_workon_home + venv = get_workon_home() / name + if not venv.exists(): + return '' + return '{0}'.format(venv) - @property - def virtualenv_name(self): + @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 # @@ -220,22 +224,38 @@ class Project(object): # 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() - if os.name == 'nt' and not PIPENV_VENV_IN_PROJECT and sanitized.lower() != sanitized: - venv = self._get_virtualenv_location(sanitized) - lower_venv = self._get_virtualenv_location(sanitized.lower()) - if not venv.strip() and lower_venv.strip(): - sanitized = sanitized.lower() + 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 + pipfile = self.pipfile_location + clean_name, encoded_hash = get_name(name, pipfile) + venv_name = '{0}-{1}'.format(clean_name, encoded_hash) + # Check for different capitalization of the same project on windows + if os.name == 'nt' and not PIPENV_VENV_IN_PROJECT and not self._get_virtualenv_location(venv_name): + from pipenv.patched.pew.pew import lsenvs + env_name = first([env for env in lsenvs() if env.lower().startswith(name.lower())]) + if env_name: + env_name = env_name[:-9] + pipfile = self.pipfile_location.replace(name, env_name) + clean_name, encoded_hash = get_name(env_name, pipfile) + 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): diff --git a/tests/test_pipenv.py b/tests/test_pipenv.py index 4d6556b3..65fe3457 100644 --- a/tests/test_pipenv.py +++ b/tests/test_pipenv.py @@ -1320,3 +1320,19 @@ multicommand = "bash -c \"cd docs && make html\"" 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() From a895218013f6937a3e7361d82f06402cc0063c4e Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung Date: Wed, 11 Apr 2018 22:46:58 +0800 Subject: [PATCH 3/3] Resolve Pipfile location for hash caculation This should prevent the lookup loop most of the time, especially for new virtualenvs created from now on. --- pipenv/project.py | 51 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/pipenv/project.py b/pipenv/project.py index f9177d0a..1529068d 100644 --- a/pipenv/project.py +++ b/pipenv/project.py @@ -11,11 +11,15 @@ import hashlib import contoml import delegator -from pipenv.vendor.first import first import pipfile 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 ( @@ -43,12 +47,19 @@ from .environments import ( 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 = {} @@ -235,20 +246,32 @@ class Project(object): name = self._sanitize(name) hash = hashlib.sha256(location.encode()).digest()[:6] encoded_hash = base64.urlsafe_b64encode(hash).decode() - return name, encoded_hash - pipfile = self.pipfile_location - clean_name, encoded_hash = get_name(name, pipfile) + return name, encoded_hash[:8] + + clean_name, encoded_hash = get_name(name, self.pipfile_location) venv_name = '{0}-{1}'.format(clean_name, encoded_hash) - # Check for different capitalization of the same project on windows - if os.name == 'nt' and not PIPENV_VENV_IN_PROJECT and not self._get_virtualenv_location(venv_name): - from pipenv.patched.pew.pew import lsenvs - env_name = first([env for env in lsenvs() if env.lower().startswith(name.lower())]) - if env_name: - env_name = env_name[:-9] - pipfile = self.pipfile_location.replace(name, env_name) - clean_name, encoded_hash = get_name(env_name, pipfile) + + # 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) @@ -326,7 +349,7 @@ class Project(object): 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