Repository: kennethreitz/pipenv
Pull request #242 (2017-02-25 16:58:01)

Fix VCS dependencies
~This is **DO NOT MERGE** until at least later today. Still need to find some time to clean up but want to allow some time for review from anyone who's interested.~

This is an attempt to address #204, #229 and #240 which all have the same underlying issue. When dealing with Pipfile and Pipfile.lock entries we've had a lot of issues with casing and name formatting in comparisons. We'd fixed this previously for package names specified by us, or pipenv users, but missed the case with mis-cased names in other packages' `install_requires` lists.

In order to prevent this from occurring in the future, I've transitioned to using a normalized naming scheme derived from pep426 where all names will be lower-cased and use dashes in place of underscores. This should allow uniform comparison and then we will rewrite the file after passing it through `proper_case` on the way out to ensure "pretty" package names.

This fixes part of the VCS problem but exposed another. We currently require hashes for ALL dependencies which pip can't support for packages that aren't hosted on PyPI. Due to this requirement, we will now split vcs dependencies off from PyPI packages before install to keep things working correctly.

This still has a few cosmetic/organization changes that need to be made, along with some testing for the new functions, but this "works" right now. If anyone has comments or wants to test this out you can clone the repository, run `git fetch origin pull/242/head:fix_vcs_dependencies`, and checkout the branch `fix_vcs_dependencies`.


Patch:
diff --git a/Pipfile b/Pipfile
index e6c64974..ecb0aa45 100644
--- a/Pipfile
+++ b/Pipfile
@@ -1,7 +1,7 @@
 [dev-packages]
 pytest = "*"
-sphinx = "*"
 mock = "*"
+Sphinx = "*"
 
 [packages]
 click = "*"
@@ -14,9 +14,9 @@ parse = "*"
 pipfile = "==0.0.1"
 click-completion = "*"
 psutil = "*"
-"backports.shutil-get-terminal-size" = "*"
 pew = ">=0.1.26"
 blindspin = "*"
+"backports.shutil_get_terminal_size" = "*"
 
 [requires]
 python_version = "2.7"
diff --git a/Pipfile.lock b/Pipfile.lock
index 7e2754ee..fd2e60b5 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -8,17 +8,21 @@
             "version": "==0.1.0",
             "hash": "sha256:fee2380a469ffe4067bc7f0096a6fcfb27539da7496fae12b74b8d5d0f33a4ee"
         },
-        "backports.shutil-get-terminal-size": {
-            "version": "==1.0.0",
-            "hash": "sha256:0975ba55054c15e346944b38956a4c9cbee9009391e41b86c68990effb8c1f64"
+        "requests": {
+            "version": "==2.13.0",
+            "hash": "sha256:1a720e8862a41aa22e339373b526f508ef0c8988baf48b84d3fc891a8e237efb"
         },
         "blindspin": {
             "version": "==2.0.1",
             "hash": "sha256:b30ef7a26ef1637c1d047667b279e8c15dc2a78fcfaad6b3027d7217e752bdba"
         },
-        "parse": {
-            "version": "==1.6.6",
-            "hash": "sha256:a4862be306f334c36ae7adc73af028c56ca0139b8e39435e935bde8d481dd99e"
+        "Jinja2": {
+            "version": "==2.9.5",
+            "hash": "sha256:a7b7438120dbe76a8e735ef7eba6048eaf4e0b7dbc530e100812f8ec462a4d50"
+        },
+        "packaging": {
+            "version": "==16.8",
+            "hash": "sha256:99276dc6e3a7851f32027a68f1095cd3f77c148091b092ea867a351811cfe388"
         },
         "MarkupSafe": {
             "version": "==0.23",
@@ -36,21 +40,21 @@
             "version": "==0.2.1",
             "hash": "sha256:079fb138887d4de12a0b7fbebf8d92d396b7c1a9c49f63475d9f3909d2588976"
         },
-        "pipfile": {
-            "version": "==0.0.1",
-            "hash": "sha256:a0bf9924d9bd25fa5e216d643fde49ff67f48727cb711a02ad806bf7c4722ea2"
+        "click": {
+            "version": "==6.7",
+            "hash": "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d"
         },
         "appdirs": {
-            "version": "==1.4.0",
-            "hash": "sha256:85e58578db8f29538f3109c11250c2a5514a2fcdc9890d9b2fe777eb55517736"
+            "version": "==1.4.2",
+            "hash": "sha256:a53330b9d53b66aba1e26907dea2958982ebad92735f9faf3897b73c909a20c1"
         },
         "virtualenv": {
             "version": "==15.1.0",
             "hash": "sha256:39d88b533b422825d644087a21e78c45cf5af0ef7a99a1fc9fbb7b481e5c85b0"
         },
-        "packaging": {
-            "version": "==16.8",
-            "hash": "sha256:99276dc6e3a7851f32027a68f1095cd3f77c148091b092ea867a351811cfe388"
+        "parse": {
+            "version": "==1.8.0",
+            "hash": "sha256:8b4f28bbe7c0f24981669ea92b2ba704ee63b5346027e82be30118bb5788ff10"
         },
         "pexpect": {
             "version": "==4.2.1",
@@ -76,14 +80,14 @@
             "version": "==0.1.26",
             "hash": "sha256:259ac7a4603fe41b1fa950f30b2e4ccf4a23f7c89be2c34e0a4cec176c3ec581"
         },
+        "backports.shutil_get_terminal_size": {
+            "version": "==1.0.0",
+            "hash": "sha256:0975ba55054c15e346944b38956a4c9cbee9009391e41b86c68990effb8c1f64"
+        },
         "ptyprocess": {
             "version": "==0.5.1",
             "hash": "sha256:464cb76f7a7122743dd25507650db89cd447c51f38e4671602b3eaa2e38e05ae"
         },
-        "click": {
-            "version": "==6.7",
-            "hash": "sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d"
-        },
         "six": {
             "version": "==1.10.0",
             "hash": "sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1"
@@ -97,16 +101,12 @@
             "hash": "sha256:b3953bffe848ad9a6d554114d82f2dcb3e23945e90b4d9addc9956f37f336594"
         },
         "setuptools": {
-            "version": "==34.2.0",
-            "hash": "sha256:75d352eeeddd96a3fb702b1da9f8b89e5f8c045dbd86e3894516733eb5f99713"
+            "version": "==34.3.0",
+            "hash": "sha256:ef40d2d360ba1be284a47c51a5a489afa7d8cde5ca39f2321876e00e93fe04cd"
         },
-        "jinja2": {
-            "version": "==2.9.5",
-            "hash": "sha256:a7b7438120dbe76a8e735ef7eba6048eaf4e0b7dbc530e100812f8ec462a4d50"
-        },
-        "requests": {
-            "version": "==2.13.0",
-            "hash": "sha256:1a720e8862a41aa22e339373b526f508ef0c8988baf48b84d3fc891a8e237efb"
+        "pipfile": {
+            "version": "==0.0.1",
+            "hash": "sha256:a0bf9924d9bd25fa5e216d643fde49ff67f48727cb711a02ad806bf7c4722ea2"
         }
     },
     "develop": {
@@ -114,13 +114,17 @@
             "version": "==1.10.0",
             "hash": "sha256:0ff78c403d9bccf5a425a6d31a12aa6b47f1c21ca4dc2573a7e2f32a97335eb1"
         },
+        "snowballstemmer": {
+            "version": "==1.2.1",
+            "hash": "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
+        },
         "py": {
             "version": "==1.4.32",
             "hash": "sha256:2d4bba2e25fff58140e6bdce1e485e89bb59776adbe01d490baa6b1f37a3dd6b"
         },
-        "sphinx": {
-            "version": "==1.5.2",
-            "hash": "sha256:57c8636e1d23f6c01fb19911a8f255f1b4934ba69feb55bd4dd0f097ebb04f05"
+        "Sphinx": {
+            "version": "==1.5.3",
+            "hash": "sha256:2809b2ab81906483b47c9d7e28740e3b69c160617f33d7aa11dc4ae376fd04d9"
         },
         "imagesize": {
             "version": "==0.7.1",
@@ -134,11 +138,7 @@
             "version": "==0.23",
             "hash": "sha256:a4ec1aff59b95a14b45eb2e23761a0179e98319da5a7eb76b56ea8cdc7b871c3"
         },
-        "snowballstemmer": {
-            "version": "==1.2.1",
-            "hash": "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
-        },
-        "babel": {
+        "Babel": {
             "version": "==2.3.4",
             "hash": "sha256:3318ed2960240d61cbc6558858ee00c10eed77a6508c4d1ed8e6f7f48399c975"
         },
@@ -150,10 +150,6 @@
             "version": "==1.10.0",
             "hash": "sha256:f5cf7265a80636ecff66806d13494cbf9d77a3758a65fd8b4d4d4bee81b0c375"
         },
-        "pytest": {
-            "version": "==3.0.6",
-            "hash": "sha256:da0ab50c7eec0683bc24f1c1137db1f4111752054ecdad63125e7ec71316b813"
-        },
         "packaging": {
             "version": "==16.8",
             "hash": "sha256:99276dc6e3a7851f32027a68f1095cd3f77c148091b092ea867a351811cfe388"
@@ -162,21 +158,25 @@
             "version": "==0.13.1",
             "hash": "sha256:cb3ebcb09242804f84bdbf0b26504077a054da6772c6f4d625f335cc53ebf94d"
         },
-        "Pygments": {
-            "version": "==2.2.0",
-            "hash": "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d"
+        "pytest": {
+            "version": "==3.0.6",
+            "hash": "sha256:da0ab50c7eec0683bc24f1c1137db1f4111752054ecdad63125e7ec71316b813"
         },
         "Jinja2": {
             "version": "==2.9.5",
             "hash": "sha256:a7b7438120dbe76a8e735ef7eba6048eaf4e0b7dbc530e100812f8ec462a4d50"
         },
         "appdirs": {
-            "version": "==1.4.0",
-            "hash": "sha256:85e58578db8f29538f3109c11250c2a5514a2fcdc9890d9b2fe777eb55517736"
+            "version": "==1.4.2",
+            "hash": "sha256:a53330b9d53b66aba1e26907dea2958982ebad92735f9faf3897b73c909a20c1"
         },
         "setuptools": {
-            "version": "==34.2.0",
-            "hash": "sha256:75d352eeeddd96a3fb702b1da9f8b89e5f8c045dbd86e3894516733eb5f99713"
+            "version": "==34.3.0",
+            "hash": "sha256:ef40d2d360ba1be284a47c51a5a489afa7d8cde5ca39f2321876e00e93fe04cd"
+        },
+        "Pygments": {
+            "version": "==2.2.0",
+            "hash": "sha256:78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d"
         },
         "pyparsing": {
             "version": "==2.1.10",
@@ -202,7 +202,7 @@
             "python_version": "2.7"
         },
         "hash": {
-            "sha256": "c1291a920a500f47bed4d4e22c0cc8354cb2b72304010a05e3ef0aed0c4e06cd"
+            "sha256": "33bb704e3081ebcb3c18c3a36ef4b26218e52e2f1075c34b1a11ac0d18198d42"
         }
     }
 }
\ No newline at end of file
diff --git a/pipenv/cli.py b/pipenv/cli.py
index 400ada6e..568ac398 100644
--- a/pipenv/cli.py
+++ b/pipenv/cli.py
@@ -21,16 +21,12 @@ from blindspin import spinner
 from requests.packages.urllib3.exceptions import InsecureRequestWarning
 
 from .project import Project
-from .utils import convert_deps_from_pip, convert_deps_to_pip, is_required_version
+from .utils import (convert_deps_from_pip, convert_deps_to_pip, is_required_version,
+    proper_case, pep426_name, split_vcs, recase_file)
 from .__version__ import __version__
 from . import pep508checker
 from .environments import PIPENV_COLORBLIND, PIPENV_NOSPIN, PIPENV_SHELL_COMPAT, PIPENV_VENV_IN_PROJECT
 
-try:
-    from HTMLParser import HTMLParser
-except ImportError:
-    from html.parser import HTMLParser
-
 # Backport required for earlier versions of Python.
 if sys.version_info < (3, 3):
     from backports.shutil_get_terminal_size import get_terminal_size
@@ -90,7 +86,7 @@ def ensure_pipfile(validate=True):
     # Validate the Pipfile's contents.
     if validate and project.virtualenv_exists:
         # Ensure that Pipfile is using proper casing.
-        p = project.parsed_pipfile
+        p = project._pipfile
         changed = ensure_proper_casing(pfile=p)
 
         # Write changes out to disk.
@@ -190,39 +186,41 @@ def do_install_dependencies(dev=False, only=False, bare=False, requirements=Fals
     if requirements:
         bare = True
 
-    # Load the Pipfile.
-    p = pipfile.load(project.pipfile_location)
-
     # Load the lockfile if it exists, or if only is being used (e.g. lock is being used).
     if only or not project.lockfile_exists:
         if not bare:
             click.echo(crayons.yellow('Installing dependencies from Pipfile...'))
-            lockfile = json.loads(p.lock())
+            lockfile = split_vcs(project._lockfile)
     else:
         if not bare:
             click.echo(crayons.yellow('Installing dependencies from Pipfile.lock...'))
         with open(project.lockfile_location) as f:
-            lockfile = json.load(f)
+            lockfile = split_vcs(json.load(f))
 
     # Install default dependencies, always.
     deps = lockfile['default'] if not only else {}
+    vcs_deps = lockfile.get('default-vcs', {})
 
     # Add development deps if --dev was passed.
     if dev:
         deps.update(lockfile['develop'])
+        vcs_deps.update(lockfile.get('develop-vcs', {}))
 
     # Convert the deps to pip-compatible arguments.
-    deps_path = convert_deps_to_pip(deps)
+    hashed_deps_path = convert_deps_to_pip(deps)
+    vcs_deps_path = convert_deps_to_pip(vcs_deps)
 
     # --requirements was passed.
     if requirements:
-        with open(deps_path) as f:
+        with open(hashed_deps_path) as f:
             click.echo(f.read())
-            sys.exit(0)
+        with open(vcs_deps_path) as f:
+            click.echo(f.read())
+        sys.exit(0)
 
     # pip install:
     with spinner():
-        c = pip_install(r=deps_path, allow_global=allow_global)
+        c = pip_install(r=hashed_deps_path, require_hashes=True, allow_global=allow_global)
 
     if c.return_code != 0:
         click.echo(crayons.red('An error occured while installing!'))
@@ -230,23 +228,33 @@ def do_install_dependencies(dev=False, only=False, bare=False, requirements=Fals
         sys.exit(c.return_code)
 
     if not bare:
-        click.echo(crayons.blue(format_pip_output(c.out, r=deps_path)))
+        click.echo(crayons.blue(format_pip_output(c.out, r=hashed_deps_path)))
+
+    with spinner():
+        c = pip_install(r=vcs_deps_path, allow_global=allow_global)
+
+    if c.return_code != 0:
+        click.echo(crayons.red('An error occured while installing!'))
+        click.echo(crayons.blue(format_pip_error(c.err)))
+        sys.exit(c.return_code)
+
+    if not bare:
+        click.echo(crayons.blue(format_pip_output(c.out, r=vcs_deps_path)))
 
     # Cleanup the temp requirements file.
     if requirements:
-        os.remove(deps_path)
+        os.remove(hashed_deps_path)
+        os.remove(vcs_deps_path)
 
 
 def do_download_dependencies(dev=False, only=False, bare=False):
     """"Executes the download functionality."""
 
-    # Load the Pipfile.
-    p = pipfile.load(project.pipfile_location)
+    # Load the Lockfile.
+    lockfile = split_vcs(project._lockfile)
 
-    # Load the Pipfile.
     if not bare:
         click.echo(crayons.yellow('Downloading dependencies from Pipfile...'))
-    lockfile = json.loads(p.lock())
 
     # Install default dependencies, always.
     deps = lockfile['default'] if not only else {}
@@ -294,6 +302,8 @@ def parse_install_output(output):
         name = lines[0].split('(')[0]
         # Strip version specification. e.g. package; python-version=2.6
         name = name.split(';')[0]
+        # Standardize name to pep426.
+        name = pep426_name(name.strip())
 
         for line in lines:
             r = parse.parse('Saved {file}', line.strip())
@@ -306,7 +316,7 @@ def parse_install_output(output):
             # Unencode percent-encoded values like ``!`` in version number.
             fname = requests.compat.unquote(fname)
 
-            names.append((fname, name.strip()))
+            names.append((fname, name))
             break
 
     return names
@@ -343,10 +353,6 @@ def do_create_virtualenv(three=None, python=None):
     do_where(virtualenv=True, bare=False)
 
 
-def is_version(text):
-    return re.match('^[\d]+\.[\d]+.*', text) or False
-
-
 def parse_download_fname(fname, name):
     fname, fextension = os.path.splitext(fname)
 
@@ -399,16 +405,13 @@ def do_lock():
         names_map = do_download_dependencies(dev=True, only=True, bare=True)
 
     # Load the Pipfile and generate a lockfile.
-    p = pipfile.load(project.pipfile_location)
-    lockfile = json.loads(p.lock())
+    p = project._pipfile
+    lockfile = project._lockfile
 
     # Pip freeze development dependencies.
     with spinner():
         results = get_downloads_info(names_map, 'dev-packages')
 
-    # Clear generated lockfile before updating.
-    lockfile['develop'] = {}
-
     # Add Development dependencies to lockfile.
     for dep in results:
         if dep:
@@ -427,14 +430,14 @@ def do_lock():
     # Pip freeze default dependencies.
     results = get_downloads_info(names_map, 'packages')
 
-    # Clear generated lockfile before updating.
-    lockfile['default'] = {}
-
     # Add default dependencies to lockfile.
     for dep in results:
         if dep:
             lockfile['default'].update({dep['name']: {'hash': dep['hash'], 'version': '=={0}'.format(dep['version'])}})
 
+    # Properly case package names.
+    lockfile = recase_file(lockfile)
+
     # Write out lockfile.
     with open(project.lockfile_location, 'w') as f:
         f.write(json.dumps(lockfile, indent=4, separators=(',', ': ')))
@@ -550,13 +553,18 @@ def do_init(dev=False, requirements=False, skip_virtualenv=False, allow_global=F
     do_activate_virtualenv()
 
 
-def pip_install(package_name=None, r=None, allow_global=False):
+def pip_install(package_name=None, r=None, allow_global=False, require_hashes=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']))
+            install_reqs = ' -r {0}'.format(r)
         else:
-            c = delegator.run('"{0}" install "{1}" -i {2}'.format(which_pip(allow_global=allow_global), package_name, source['url']))
+            install_reqs = ' "{0}"'.format(package_name)
+
+        if require_hashes:
+            install_reqs += ' --require-hashes'
+
+        c = delegator.run('"{0}" install {1} -i {2}'.format(which_pip(allow_global=allow_global), install_reqs, source['url']))
 
         if c.return_code == 0:
             break
@@ -585,33 +593,6 @@ def which_pip(allow_global=False):
     return which('pip')
 
 
-def proper_case(package_name):
-
-    # Capture tag contents here.
-    collected = []
-
-    class SimpleHTMLParser(HTMLParser):
-        def handle_data(self, data):
-            # Remove extra blank data from https://pypi.org/simple
-            data = data.strip()
-            if len(data) > 2:
-                collected.append(data)
-
-    # Hit the simple API.
-    r = requests.get('https://pypi.org/simple/{0}'.format(package_name))
-    if not r.ok:
-        raise IOError('Unable to find package {0} in PyPI repository.'.format(crayons.green(package_name)))
-
-    # Parse the HTML.
-    parser = SimpleHTMLParser()
-    parser.feed(r.text)
-
-    r = parse.parse('Links for {name}', collected[1])
-    good_name = r['name']
-
-    return good_name
-
-
 def format_help(help):
     """Formats the help string."""
     help = help.replace('  check', str(crayons.green('  check')))
@@ -778,13 +759,16 @@ def install(package_name=False, more_packages=False, dev=False, three=False, pyt
             click.echo('Adding {0} to Pipfile\'s {1}...'.format(crayons.green(package_name), crayons.red('[packages]')))
 
         # Add the package to the Pipfile.
-        project.add_package_to_pipfile(package_name, dev)
+        try:
+            project.add_package_to_pipfile(package_name, dev)
+        except ValueError as e:
+            click.echo('{0} {1}'.format(crayons.red('ERROR (PACKAGE NOT INSTALLED):'), e))
 
         # Ego boost.
         easter_egg(package_name)
 
-        if lock:
-            do_lock()
+    if lock:
+        do_lock()
 
 
 @click.command(help="Un-installs a provided package and removes it from Pipfile, or (if none is given), un-installs all packages.")
diff --git a/pipenv/project.py b/pipenv/project.py
index 965b3403..f0f2c3d0 100644
--- a/pipenv/project.py
+++ b/pipenv/project.py
@@ -11,8 +11,8 @@ import toml
 import delegator
 from requests.compat import OrderedDict
 
-from .utils import format_toml, mkdir_p
-from .utils import convert_deps_from_pip
+from .utils import (format_toml, mkdir_p, convert_deps_from_pip,
+    proper_case, pep426_name, is_vcs, recase_file)
 from .environments import PIPENV_MAX_DEPTH, PIPENV_VENV_IN_PROJECT
 
 
@@ -130,6 +130,35 @@ class Project(object):
         with open(self.pipfile_location) as f:
             return toml.load(f, _dict=OrderedDict)
 
+    @property
+    def _pipfile(self):
+        """Pipfile divided by PyPI and external dependencies."""
+        pfile = self.parsed_pipfile
+        for section in ('packages', 'dev-packages'):
+            p_section = pfile.get(section, {})
+
+            for key in list(p_section.keys()):
+                # Normalize key name to pep426.
+                norm_key = pep426_name(key)
+                p_section[norm_key] = p_section.pop(key)
+
+        return pfile
+
+    @property
+    def _lockfile(self):
+        """Pipfile.lock divided by PyPI and external dependencies."""
+        pfile = pipfile.load(self.pipfile_location)
+        lockfile = json.loads(pfile.lock())
+
+        for section in ('default', 'develop'):
+            lock_section = lockfile.get(section, {})
+
+            for key in list(lock_section.keys()):
+                norm_key = pep426_name(key)
+                lockfile[section][norm_key] = lock_section.pop(key)
+
+        return lockfile
+
     @property
     def lockfile_location(self):
         return '{0}.lock'.format(self.pipfile_location)
@@ -170,21 +199,22 @@ class Project(object):
     def remove_package_from_pipfile(self, package_name, dev=False):
 
         # Read and append Pipfile.
-        p = self.parsed_pipfile
+        p = self._pipfile
+
+        package_name = pep426_name(package_name)
 
         key = 'dev-packages' if dev else 'packages'
 
-        if key in p:
-            if package_name in p[key]:
-                del p[key][package_name]
+        if key in p and package_name in p[key]:
+            del p[key][package_name]
 
         # Write Pipfile.
-        self.write_toml(p)
+        self.write_toml(recase_file(p))
 
     def add_package_to_pipfile(self, package_name, dev=False):
 
         # Read and append Pipfile.
-        p = self.parsed_pipfile
+        p = self._pipfile
 
         key = 'dev-packages' if dev else 'packages'
 
@@ -199,4 +229,4 @@ class Project(object):
         p[key][package_name] = package[package_name]
 
         # Write Pipfile.
-        self.write_toml(p)
+        self.write_toml(recase_file(p))
diff --git a/pipenv/utils.py b/pipenv/utils.py
index 8c50a107..0ccf8c4e 100644
--- a/pipenv/utils.py
+++ b/pipenv/utils.py
@@ -2,8 +2,17 @@
 import os
 import tempfile
 
+import parse
+import requests
 import requirements
 
+try:
+    from HTMLParser import HTMLParser
+except ImportError:
+    from html.parser import HTMLParser
+
+# List of version control systems we support.
+VCS_LIST = ('git', 'svn', 'hg', 'bzr')
 
 def format_toml(data):
     """Pretty-formats a given toml string."""
@@ -42,6 +51,11 @@ def convert_deps_from_pip(dep):
 
     # VCS Installs.
     elif req.vcs:
+        if req.name is None:
+            raise ValueError('pipenv requires an #egg fragment for version controlled '
+                             'dependencies. Please install remote dependency '
+                             'in the form {0}#egg=<package-name>.'.format(req.uri))
+
         # Crop off the git+, etc part.
         dependency[req.name] = {req.vcs: req.uri[len(req.vcs) + 1:]}
 
@@ -85,7 +99,7 @@ def convert_deps_to_pip(deps, r=True):
             version = deps[dep]['version']
 
         # Support for version control
-        maybe_vcs = [vcs for vcs in ('git', 'svn', 'hg', 'bzr') if vcs in deps[dep]]
+        maybe_vcs = [vcs for vcs in VCS_LIST if vcs in deps[dep]]
         vcs = maybe_vcs[0] if maybe_vcs else None
 
         if vcs:
@@ -144,3 +158,79 @@ def is_required_version(version, specified_version):
     if specified_version.startswith('=='):
         return version.strip() == specified_version.split('==')[1].strip()
     return True
+
+
+def is_vcs(pipfile_entry):
+    """Determine if dictionary entry from Pipfile is for a vcs dependency."""
+    if isinstance(pipfile_entry, dict):
+        return any(key for key in pipfile_entry.keys() if key in VCS_LIST)
+    return False
+
+
+def pep426_name(name):
+    """Normalize package name to pep426 style standard."""
+    return name.lower().replace('_','-')
+
+
+def proper_case(package_name):
+    """Properly case project name from pypi.org"""
+    # Capture tag contents here.
+    collected = []
+
+    class SimpleHTMLParser(HTMLParser):
+        def handle_data(self, data):
+            # Remove extra blank data from https://pypi.org/simple
+            data = data.strip()
+            if len(data) > 2:
+                collected.append(data)
+
+    # Hit the simple API.
+    r = requests.get('https://pypi.org/simple/{0}'.format(package_name))
+    if not r.ok:
+        raise IOError('Unable to find package {0} in PyPI repository.'.format(package_name))
+
+    # Parse the HTML.
+    parser = SimpleHTMLParser()
+    parser.feed(r.text)
+
+    r = parse.parse('Links for {name}', collected[1])
+    good_name = r['name']
+
+    return good_name
+
+
+def split_vcs(split_file):
+    """Split VCS dependencies out from file."""
+    if 'packages' in split_file or 'dev-packages' in split_file:
+        sections = ('packages', 'dev-packages')
+    elif 'default' in split_file or 'develop' in split_file:
+        sections = ('default', 'develop')
+
+     # For each vcs entry in a given section, move it to section-vcs.
+    for section in sections:
+        entries = split_file.get(section, {})
+        vcs_dict = dict((k, entries.pop(k)) for k in list(entries.keys()) if is_vcs(entries[k]))
+        split_file[section+'-vcs'] = vcs_dict
+
+    return split_file
+
+
+def recase_file(file_dict):
+    """Recase file before writing to output."""
+    if 'packages' in file_dict or 'dev-packages' in file_dict:
+        sections = ('packages', 'dev-packages')
+    elif 'default' in file_dict or 'develop' in file_dict:
+        sections = ('default', 'develop')
+
+    for section in sections:
+        file_section = file_dict.get(section, {})
+
+        # Try to properly case each key if we can.
+        for key in list(file_section.keys()):
+            try:
+                cased_key = proper_case(key)
+            except IOError:
+                cased_key = key
+            file_section[cased_key] = file_section.pop(key)
+
+    return file_dict


Test patch:
diff --git a/tests/test_pipenv.py b/tests/test_pipenv.py
index d05be191..05b6ab4d 100644
--- a/tests/test_pipenv.py
+++ b/tests/test_pipenv.py
@@ -39,10 +39,19 @@ class TestPipenv():
         assert delegator.run('pipenv --python python').return_code == 0
         assert delegator.run('pipenv install requests').return_code == 0
         assert delegator.run('pipenv install pytest --dev').return_code == 0
+        assert delegator.run('pipenv install git+https://github.com/kennethreitz/records.git@v0.5.0#egg=records').return_code == 0
         assert delegator.run('pipenv lock').return_code == 0
 
-        assert 'pytest' in delegator.run('cat Pipfile').out
-        assert 'pytest' in delegator.run('cat Pipfile.lock').out
+        pipfile_output = delegator.run('cat Pipfile').out
+        lockfile_output = delegator.run('cat Pipfile.lock').out
+
+        # Ensure dev-packages work.
+        assert 'pytest' in pipfile_output
+        assert 'pytest' in lockfile_output
+
+        # Ensure vcs dependencies work.
+        assert 'records' in pipfile_output
+        assert '"git": "https://github.com/kennethreitz/records.git"' in lockfile_output
 
         os.chdir('..')
         delegator.run('rm -fr test_project')
diff --git a/tests/test_project.py b/tests/test_project.py
index 1c73d688..f5a78e43 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -64,6 +64,7 @@ class TestProject():
 
         proj.add_package_to_pipfile('Flask')
         proj.add_package_to_pipfile('Django==1.10.1', dev=True)
+        proj.add_package_to_pipfile('Click-ComPletiON')
         p = proj.parsed_pipfile
 
         # Cleanup test space.
@@ -77,6 +78,10 @@ class TestProject():
         assert 'Django' in p['dev-packages']
         assert p['dev-packages']['Django'] == '==1.10.1'
 
+        # Confirm casing is normalized.
+        assert 'click-completion' in p['packages']
+        assert p['packages']['click-completion'] == '*'
+
     def test_remove_package_from_pipfile(self):
         proj = pipenv.project.Project()
 
@@ -86,17 +91,18 @@ class TestProject():
             f.write('[[source]]\nurl = \'https://pypi.python.org/simple\'\n'
                     'verify_ssl = true\n\n\n[packages]\n'
                     'requests = { extras = [\'socks\'] }\nFlask = \'*\'\n\n\n'
-                    '[dev-packages]\nclick = \'*\'\n')
+                    '[dev-packages]\nclick = \'*\'\nDjango = \'*\'\n')
         proj._pipfile_location = 'test_remove_from_pipfile/Pipfile'
 
         # Confirm initial state of Pipfile.
         p = proj.parsed_pipfile
         assert list(p['packages'].keys()) == ['requests', 'Flask']
-        assert list(p['dev-packages'].keys()) == ['click']
+        assert list(p['dev-packages'].keys()) == ['click', 'Django']
 
         # Remove requests from packages and click from dev-packages.
         proj.remove_package_from_pipfile('requests')
         proj.remove_package_from_pipfile('click', dev=True)
+        proj.remove_package_from_pipfile('DJANGO', dev=True)
         p = proj.parsed_pipfile
 
         # Cleanup test space.
@@ -107,3 +113,56 @@ class TestProject():
         assert len(p['packages']) == 1
 
         assert 'dev-packages' not in p
+
+    def test_internal_pipfile(self):
+        proj = pipenv.project.Project()
+
+        # Create test space.
+        delegator.run('mkdir test_internal_pipfile')
+        with open('test_internal_pipfile/Pipfile', 'w') as f:
+            f.write('[[source]]\nurl = \'https://pypi.python.org/simple\'\n'
+                    'verify_ssl = true\n\n\n[packages]\n'
+                    'Requests = { extras = [\'socks\'] }\nFlask_Auth = \'*\'\n\n\n'
+                    '[dev-packages]\nclick = \'*\'\nDjango = {git = '
+                    '"https://github.com/django/django.git", ref="1.10"}\n')
+
+        proj._pipfile_location = 'test_internal_pipfile/Pipfile'
+
+        p = proj._pipfile
+
+        # Test package names are normalized as expected.
+        assert list(p['packages'].keys()) == ['requests', 'flask-auth']
+        assert list(p['dev-packages'].keys()) == ['click', 'django']
+
+        delegator.run('rm -fr test_internal_pipfile')
+
+    def test_internal_lockfile(self):
+        proj = pipenv.project.Project()
+
+        # Create test space.
+        delegator.run('mkdir test_internal_lockfile')
+
+        with open('test_internal_lockfile/Pipfile', 'w') as f:
+            f.write('[[source]]\nurl = \'https://pypi.python.org/simple\'\n'
+                    'verify_ssl = true\n\n\n[packages]\n'
+                    'Requests = { extras = [\'socks\'] }\nFlask_Auth = \'*\'\n\n\n'
+                    '[dev-packages]\nclick = \'*\'\nDjango = {git = '
+                    '"https://github.com/django/django.git", ref="1.10"}\n')
+
+        proj._pipfile_location = 'test_internal_lockfile/Pipfile'
+
+        lockfile = proj._lockfile
+
+        # Verify default section of lockfile.
+        assert len(lockfile['default'].keys()) == 2
+        assert 'requests' in lockfile['default']
+        assert 'flask-auth' in lockfile['default']
+
+        # Verify develop section of lockfile.
+        assert lockfile['develop']['django']['git'] == 'https://github.com/django/django.git'
+        assert lockfile['develop']['click'] == '*'
+
+        # Verify _meta exists.
+        assert lockfile['_meta']['hash'] == {'sha256': 'ff0b0584610a7091156f32ca7d5adab8f29cb17263c6d63bcab42de2137c4787'}
+
+        delegator.run('rm -fr test_internal_lockfile')
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 6d45102e..0f3a2d81 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -83,6 +83,12 @@ class TestUtils:
         dep = pipenv.utils.convert_deps_from_pip(dep)
         assert dep == {'MyProject': {'hg': 'http://hg.myproject.org/MyProject', 'ref': 'da39a3ee5e6b'}}
 
+        # vcs dependency without #egg
+        dep = 'git+https://github.com/kennethreitz/requests.git'
+        with pytest.raises(ValueError) as e:
+            dep = pipenv.utils.convert_deps_from_pip(dep)
+            assert 'pipenv requires an #egg fragment for vcs' in str(e)
+
 
     @pytest.mark.parametrize('version, specified_ver, expected', [
         ('*', '*', True),
@@ -93,3 +99,34 @@ class TestUtils:
     ])
     def test_is_required_version(self, version, specified_ver, expected):
         assert pipenv.utils.is_required_version(version, specified_ver) is expected
+
+
+    @pytest.mark.parametrize('entry, expected', [
+        ({'git': 'package.git', 'ref': 'v0.0.1'}, True),
+        ({'hg': 'https://package.com/package', 'ref': 'v1.2.3'}, True),
+        ('*', False),
+        ({'some_value': 5, 'other_value': object()}, False),
+        ('package', False)
+    ])
+    def test_is_vcs(self, entry, expected):
+        assert pipenv.utils.is_vcs(entry) is expected
+
+
+    def test_split_vcs(self):
+        pipfile_dict = {
+                        'packages': {
+                            'requests': {'git': 'https://github.com/kennethreitz/requests.git'},
+                            'Flask': '*'
+                        },
+                        'dev-packages': {
+                            'Django': '==1.10',
+                            'click': {'svn': 'https://svn.notareal.com/click'},
+                            'crayons': {'hg': 'https://hg.alsonotreal.com/crayons'}
+                        }}
+        split_dict = pipenv.utils.split_vcs(pipfile_dict)
+
+        assert list(split_dict['packages'].keys()) == ['Flask']
+        assert split_dict['packages-vcs'] == {'requests': {'git': 'https://github.com/kennethreitz/requests.git'}}
+        assert list(split_dict['dev-packages'].keys()) == ['Django']
+        assert 'click' in split_dict['dev-packages-vcs']
+        assert 'crayons' in split_dict['dev-packages-vcs']