Repository: pypa/pipenv

Issue:
Cannot install a wheel package in a directory with whitespaces
### Summary

- If the name of current working directory contains the whitespaces
- You won't be able to install a package with [the wheel format](https://www.python.org/dev/peps/pep-0427/).

This bug occurs regardless of the value of `PIPENV_VENV_IN_PROJECT` environment variable.

### How to reproduce this error
Please comment if you cannot reproduce the error.

```console
$ mkdir 'directory with spaces'
$ cd 'directory with spaces/'


$ pipenv --three
Creating a Pipfile for this project...
Creating a virtualenv for this project...
⠋Running virtualenv with interpreter /usr/bin/python3
Using base prefix '/usr'
New python executable in /home/simnalamburt/.local/share/virtualenvs/directory with spaces/bin/python3
Also creating executable in /home/simnalamburt/.local/share/virtualenvs/directory with spaces/bin/python
Installing setuptools, pip, wheel...done.

Virtualenv location:


$ pipenv install numpy
Creating a virtualenv for this project...
⠋Using base prefix '/usr'
New python executable in /home/simnalamburt/.local/share/virtualenvs/directory with spaces/bin/python
Traceback (most recent call last):
  File "/usr/bin/virtualenv", line 11, in <module>
    sys.exit(main())
  File "/usr/lib/python3.6/site-packages/virtualenv.py", line 713, in main
    symlink=options.symlink)
  File "/usr/lib/python3.6/site-packages/virtualenv.py", line 925, in create_environment
    site_packages=site_packages, clear=clear, symlink=symlink))
  File "/usr/lib/python3.6/site-packages/virtualenv.py", line 1370, in install_python
    os.symlink(py_executable_base, full_pth)
FileExistsError: [Errno 17] File exists: 'python' -> '/home/simnalamburt/.local/share/virtualenvs/directory with spaces/bin/python3.6'

Virtualenv location:
Installing numpy...
Collecting numpy
  Using cached numpy-1.12.0-cp36-cp36m-manylinux1_x86_64.whl
Installing collected packages: numpy

Error:  An error occurred while installing numpy!
Exception:
Traceback (most recent call last):
  File "/usr/lib/python3.6/site-packages/pip/basecommand.py", line 215, in main
    status = self.run(options, args)
  File "/usr/lib/python3.6/site-packages/pip/commands/install.py", line 342, in run
    prefix=options.prefix_path,
  File "/usr/lib/python3.6/site-packages/pip/req/req_set.py", line 784, in install
    **kwargs
  File "/usr/lib/python3.6/site-packages/pip/req/req_install.py", line 851, in install
    self.move_wheel_files(self.source_dir, root=root, prefix=prefix)
  File "/usr/lib/python3.6/site-packages/pip/req/req_install.py", line 1064, in move_wheel_files
    isolated=self.isolated,
  File "/usr/lib/python3.6/site-packages/pip/wheel.py", line 345, in move_wheel_files
    clobber(source, lib_dir, True)
  File "/usr/lib/python3.6/site-packages/pip/wheel.py", line 316, in clobber
    ensure_dir(destdir)
  File "/usr/lib/python3.6/site-packages/pip/utils/__init__.py", line 83, in ensure_dir
    os.makedirs(path)
  File "/usr/lib/python3.6/os.py", line 220, in makedirs
    mkdir(name, mode)
PermissionError: [Errno 13] Permission denied: '/usr/lib/python3.6/site-packages/numpy-1.12.0.dist-info'
```
<img alt="screenshot" src="https://cloud.githubusercontent.com/assets/4435445/23003156/21eb4756-f432-11e6-9d65-5c45c2e5572a.png" height=200>
*(Click to see the screenshot)*

### My Environment
I could reproduce the error in multiple environments. Please comment if you want more information.

```console
$ uname -a
Linux 4.9.6-1-ARCH #1 SMP PREEMPT Thu Jan 26 09:22:26 CET 2017 x86_64 GNU/Linux
$ python3 --version
Python 3.6.0
$ pipenv --version
pipenv, version 3.4.1
```
```console
$ uname -a
Darwin 16.4.0 Darwin Kernel Version 16.4.0: Thu Dec 22 22:53:21 PST 2016; root:xnu-3789.41.3~3/RELEASE_X86_64 x86_64
$ python3 --version
Python 3.6.0
$ pipenv --version
pipenv, version 3.4.1
```
Cannot install a wheel package in a directory with whitespaces
### Summary

- If the name of current working directory contains the whitespaces
- You won't be able to install a package with [the wheel format](https://www.python.org/dev/peps/pep-0427/).

This bug occurs regardless of the value of `PIPENV_VENV_IN_PROJECT` environment variable.

### How to reproduce this error
Please comment if you cannot reproduce the error.

```console
$ mkdir 'directory with spaces'
$ cd 'directory with spaces/'


$ pipenv --three
Creating a Pipfile for this project...
Creating a virtualenv for this project...
⠋Running virtualenv with interpreter /usr/bin/python3
Using base prefix '/usr'
New python executable in /home/simnalamburt/.local/share/virtualenvs/directory with spaces/bin/python3
Also creating executable in /home/simnalamburt/.local/share/virtualenvs/directory with spaces/bin/python
Installing setuptools, pip, wheel...done.

Virtualenv location:


$ pipenv install numpy
Creating a virtualenv for this project...
⠋Using base prefix '/usr'
New python executable in /home/simnalamburt/.local/share/virtualenvs/directory with spaces/bin/python
Traceback (most recent call last):
  File "/usr/bin/virtualenv", line 11, in <module>
    sys.exit(main())
  File "/usr/lib/python3.6/site-packages/virtualenv.py", line 713, in main
    symlink=options.symlink)
  File "/usr/lib/python3.6/site-packages/virtualenv.py", line 925, in create_environment
    site_packages=site_packages, clear=clear, symlink=symlink))
  File "/usr/lib/python3.6/site-packages/virtualenv.py", line 1370, in install_python
    os.symlink(py_executable_base, full_pth)
FileExistsError: [Errno 17] File exists: 'python' -> '/home/simnalamburt/.local/share/virtualenvs/directory with spaces/bin/python3.6'

Virtualenv location:
Installing numpy...
Collecting numpy
  Using cached numpy-1.12.0-cp36-cp36m-manylinux1_x86_64.whl
Installing collected packages: numpy

Error:  An error occurred while installing numpy!
Exception:
Traceback (most recent call last):
  File "/usr/lib/python3.6/site-packages/pip/basecommand.py", line 215, in main
    status = self.run(options, args)
  File "/usr/lib/python3.6/site-packages/pip/commands/install.py", line 342, in run
    prefix=options.prefix_path,
  File "/usr/lib/python3.6/site-packages/pip/req/req_set.py", line 784, in install
    **kwargs
  File "/usr/lib/python3.6/site-packages/pip/req/req_install.py", line 851, in install
    self.move_wheel_files(self.source_dir, root=root, prefix=prefix)
  File "/usr/lib/python3.6/site-packages/pip/req/req_install.py", line 1064, in move_wheel_files
    isolated=self.isolated,
  File "/usr/lib/python3.6/site-packages/pip/wheel.py", line 345, in move_wheel_files
    clobber(source, lib_dir, True)
  File "/usr/lib/python3.6/site-packages/pip/wheel.py", line 316, in clobber
    ensure_dir(destdir)
  File "/usr/lib/python3.6/site-packages/pip/utils/__init__.py", line 83, in ensure_dir
    os.makedirs(path)
  File "/usr/lib/python3.6/os.py", line 220, in makedirs
    mkdir(name, mode)
PermissionError: [Errno 13] Permission denied: '/usr/lib/python3.6/site-packages/numpy-1.12.0.dist-info'
```
<img alt="screenshot" src="https://cloud.githubusercontent.com/assets/4435445/23003156/21eb4756-f432-11e6-9d65-5c45c2e5572a.png" height=200>
*(Click to see the screenshot)*

### My Environment
I could reproduce the error in multiple environments. Please comment if you want more information.

```console
$ uname -a
Linux 4.9.6-1-ARCH #1 SMP PREEMPT Thu Jan 26 09:22:26 CET 2017 x86_64 GNU/Linux
$ python3 --version
Python 3.6.0
$ pipenv --version
pipenv, version 3.4.1
```
```console
$ uname -a
Darwin 16.4.0 Darwin Kernel Version 16.4.0: Thu Dec 22 22:53:21 PST 2016; root:xnu-3789.41.3~3/RELEASE_X86_64 x86_64
$ python3 --version
Python 3.6.0
$ pipenv --version
pipenv, version 3.4.1
```


Discussion:

Resolving patch:
diff --git a/pipenv/cli.py b/pipenv/cli.py
--- 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))
 
 
@@ -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:
@@ -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:
@@ -892,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
@@ -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
--- 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,30 @@ def pipfile_exists(self):
     def virtualenv_exists(self):
         return os.path.isdir(self.virtualenv_location)
 
+    @property
+    def virtualenv_name(self):
+        # 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
+        #   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 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 +73,7 @@ def virtualenv_location(self):
 
         # 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:


Test patch:
diff --git a/tests/test_project.py b/tests/test_project.py
--- 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()