From b425977ce0201ac268a45536b06b41ad1a650501 Mon Sep 17 00:00:00 2001 From: Hyeon Kim Date: Thu, 16 Feb 2017 15:56:32 +0900 Subject: [PATCH 1/3] Double quote the paths This commit is part of the implementation of 'Plan B' mentioned at #228, but I found that it cannot solve the issue due to the bug of pip. (see pypa/pip#923) References: https://github.com/kennethreitz/pipenv/issues/228#issuecomment-280230269 https://github.com/pypa/pip/issues/923 --- pipenv/cli.py | 21 ++++++++++----------- pipenv/project.py | 2 +- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/pipenv/cli.py b/pipenv/cli.py index 944a6bc9..7aa4d2e0 100644 --- a/pipenv/cli.py +++ b/pipenv/cli.py @@ -66,13 +66,13 @@ def ensure_latest_pip(): """Updates pip to the latest version.""" # Ensure that pip is installed. - c = delegator.run('{0} install pip'.format(which_pip())) + c = delegator.run('"{0}" install pip'.format(which_pip())) # Check if version is out of date. if 'however' in c.err: # If version is out of date, update. click.echo(crayons.yellow('Pip is out of date... updating to latest.')) - c = delegator.run('{0} install pip --upgrade'.format(which_pip()), block=False) + c = delegator.run('"{0}" install pip --upgrade'.format(which_pip()), block=False) click.echo(crayons.blue(c.out)) @@ -374,7 +374,7 @@ def get_downloads_info(names_map, section): version = parse_download_fname(fname, name) # Get the hash of each file. - c = delegator.run('{0} hash {1}'.format(which_pip(), os.sep.join([project.download_location, fname]))) + c = delegator.run('"{0}" hash "{1}"'.format(which_pip(), os.sep.join([project.download_location, fname]))) hash = c.out.split('--hash=')[1].strip() # Verify we're adding the correct version from Pipfile @@ -491,7 +491,7 @@ def do_purge(bare=False, downloads=False, allow_global=False): shutil.rmtree(project.download_location) return - freeze = delegator.run('{0} freeze'.format(which_pip(allow_global=allow_global))).out + freeze = delegator.run('"{0}" freeze'.format(which_pip(allow_global=allow_global))).out installed = freeze.split() # Remove setuptools and friends from installed, if present. @@ -502,7 +502,7 @@ def do_purge(bare=False, downloads=False, allow_global=False): if not bare: click.echo('Found {0} installed package(s), purging...'.format(len(installed))) - command = '{0} uninstall {1} -y'.format(which_pip(allow_global=allow_global), ' '.join(installed)) + command = '"{0}" uninstall {1} -y'.format(which_pip(allow_global=allow_global), ' '.join(installed)) c = delegator.run(command) if not bare: @@ -554,9 +554,9 @@ def pip_install(package_name=None, r=None, allow_global=False): # try installing for each source in project.sources for source in project.sources: if r: - c = delegator.run('{0} install -r {1} --require-hashes -i {2}'.format(which_pip(allow_global=allow_global), r, source['url'])) + c = delegator.run('"{0}" install -r {1} --require-hashes -i {2}'.format(which_pip(allow_global=allow_global), r, source['url'])) else: - c = delegator.run('{0} install "{1}" -i {2}'.format(which_pip(allow_global=allow_global), package_name, source['url'])) + c = delegator.run('"{0}" install "{1}" -i {2}'.format(which_pip(allow_global=allow_global), package_name, source['url'])) if c.return_code == 0: break @@ -566,11 +566,10 @@ def pip_install(package_name=None, r=None, allow_global=False): def pip_download(package_name): for source in project.sources: - cmd = '{0} download "{1}" -i {2} -d {3}'.format(which_pip(), package_name, source['url'], project.download_location) + cmd = '"{0}" download "{1}" -i {2} -d {3}'.format(which_pip(), package_name, source['url'], project.download_location) c = delegator.run(cmd) if c.return_code == 0: break - return c @@ -829,7 +828,7 @@ def uninstall(package_name=False, more_packages=False, three=None, python=False, click.echo('Un-installing {0}...'.format(crayons.green(package_name))) - c = delegator.run('{0} uninstall {1} -y'.format(which_pip(allow_global=system), package_name)) + c = delegator.run('"{0}" uninstall {1} -y'.format(which_pip(allow_global=system), package_name)) click.echo(crayons.blue(c.out)) if pipfile_remove: @@ -972,7 +971,7 @@ def check(three=None, python=False): click.echo(crayons.yellow('Checking PEP 508 requirements...')) # Run the PEP 508 checker in the virtualenv. - c = delegator.run('{0} {1}'.format(which('python'), pep508checker.__file__.rstrip('cdo'))) + c = delegator.run('"{0}" {1}'.format(which('python'), pep508checker.__file__.rstrip('cdo'))) results = json.loads(c.out) # Load the pipfile. diff --git a/pipenv/project.py b/pipenv/project.py index 98ef6606..b11d031e 100644 --- a/pipenv/project.py +++ b/pipenv/project.py @@ -46,7 +46,7 @@ class Project(object): # The user wants the virtualenv in the project. if not PIPENV_VENV_IN_PROJECT: - c = delegator.run('pew dir {0}'.format(self.name)) + c = delegator.run('pew dir "{0}"'.format(self.name)) loc = c.out.strip() # Default mode. else: From a81197d6bf81a68d02cfcecda8a82e0de9b8be85 Mon Sep 17 00:00:00 2001 From: Hyeon Kim Date: Tue, 21 Feb 2017 09:50:32 +0900 Subject: [PATCH 2/3] Change the naming scheme of virtualenv New property has been added as discussed as https://github.com/kennethreitz/pipenv/pull/238#discussion_r102049269 project.virtualenv_name It'll be '-' where is a project name without whitespaces, and hash is base64-encoded sha256 of pipfile location. For example, if the pipfile was located at '/home/user/MY_PROJECT/Pipfile', the 'virtualenv_name' will be 'my-project-wyUfYPqE'. Closes #228 References: https://github.com/kennethreitz/pipenv/issues/228 https://github.com/kennethreitz/pipenv/pull/238 --- pipenv/cli.py | 4 ++-- pipenv/project.py | 22 +++++++++++++++++++++- tests/test_project.py | 7 +++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/pipenv/cli.py b/pipenv/cli.py index 7aa4d2e0..847f987d 100644 --- a/pipenv/cli.py +++ b/pipenv/cli.py @@ -321,7 +321,7 @@ def do_create_virtualenv(three=None, python=None): cmd = ['virtualenv', project.virtualenv_location, '--prompt=({0})'.format(project.name)] else: # Default: use pew. - cmd = ['pew', 'new', project.name, '-d'] + cmd = ['pew', 'new', project.virtualenv_name, '-d'] # Pass a Python version to virtualenv, if needed. if python: @@ -891,7 +891,7 @@ def shell(three=None, python=False, compat=False, shell_args=None): # Standard (properly configured shell) mode: else: cmd = 'pew' - args = ["workon", project.name] + args = ["workon", project.virtualenv_name] # Grab current terminal dimensions to replace the hardcoded default # dimensions of pexpect diff --git a/pipenv/project.py b/pipenv/project.py index b11d031e..e2b8de9b 100644 --- a/pipenv/project.py +++ b/pipenv/project.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- import json import os +import re +import base64 +import hashlib import pipfile import toml @@ -37,6 +40,23 @@ class Project(object): def virtualenv_exists(self): return os.path.isdir(self.virtualenv_location) + @property + def virtualenv_name(self): + # Replace dangerous characters into '_' + # + # References: + # https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html + # http://www.tldp.org/LDP/abs/html/special-chars.html#FIELDREF + sanitized = re.sub(r'[ $`!*@"\\\r\n\t]', '_', self.name) + + # Hash the full path of the pipfile + hash = hashlib.sha256(self.pipfile_location.encode()).digest()[:6] + encoded_hash = base64.urlsafe_b64encode(hash).decode() + + # If the pipfile was located at '/home/user/MY_PROJECT/Pipfile', + # the name of its virtualenv will be 'my-project-wyUfYPqE' + return sanitized + '-' + encoded_hash + @property def virtualenv_location(self): @@ -46,7 +66,7 @@ class Project(object): # The user wants the virtualenv in the project. if not PIPENV_VENV_IN_PROJECT: - c = delegator.run('pew dir "{0}"'.format(self.name)) + c = delegator.run('pew dir "{0}"'.format(self.virtualenv_name)) loc = c.out.strip() # Default mode. else: diff --git a/tests/test_project.py b/tests/test_project.py index fa4ede12..1c73d688 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -1,3 +1,6 @@ +from hashlib import sha256 +from base64 import urlsafe_b64encode + import delegator import pipenv.project @@ -6,9 +9,13 @@ class TestProject(): def test_project(self): proj = pipenv.project.Project() + hash = urlsafe_b64encode( + sha256(proj.pipfile_location.encode()).digest()[:6]).decode() + assert proj.name == 'pipenv' assert proj.pipfile_exists assert proj.virtualenv_exists + assert proj.virtualenv_name == 'pipenv-' + hash def test_proper_names(self): proj = pipenv.project.Project() From ed6b73c84f16e44488473c5d461b2ddfe3df6eb3 Mon Sep 17 00:00:00 2001 From: Hyeon Kim Date: Tue, 21 Feb 2017 14:19:09 +0900 Subject: [PATCH 3/3] Limit the maximum length of virtualenv_name --- pipenv/project.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pipenv/project.py b/pipenv/project.py index e2b8de9b..965b3403 100644 --- a/pipenv/project.py +++ b/pipenv/project.py @@ -42,12 +42,19 @@ class Project(object): @property def virtualenv_name(self): - # Replace dangerous characters into '_' + # Replace dangerous characters into '_'. The length of the sanitized + # project name is limited as 42 because of the limit of linux kernel + # + # 42 = 127 - len('/home//.local/share/virtualenvs//bin/python2') - 32 - len('-HASHHASH') + # + # 127 : BINPRM_BUF_SIZE - 1 + # 32 : Maxmimum length of username # # References: # https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html # http://www.tldp.org/LDP/abs/html/special-chars.html#FIELDREF - sanitized = re.sub(r'[ $`!*@"\\\r\n\t]', '_', self.name) + # 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]