Repository: pypa/pipenv

Issue:
Version resolution mismatch between PyPI and pip for non-semver packages causes keyerrors when installing dependencies
The pip version resolver strips leading zeroes for semver compliance when parsing version information for packages, but the pypi json api retains them.  

```bash
 ~/g/pipenv   master $+…  vf tmp --python=python3.6          
Running virtualenv with interpreter /usr/bin/python3.6
Using base prefix '/usr'
New python executable in /home/hawk/.virtualenvs/tempenv-18a097382f8c/bin/python3.6
Also creating executable in /home/hawk/.virtualenvs/tempenv-18a097382f8c/bin/python
Installing setuptools, pip, wheel...done.
◰³ tempenv-18a097382f8c  ~/g/pipenv   master $+…  pip3.6 install pipenv 
◰³ tempenv-18a097382f8c  ~/g/pipenv   master $+…  cd /tmp/
◰³ tempenv-18a097382f8c  /tmp  mkdir newdir
◰³ tempenv-18a097382f8c  /tmp  cd newdir
◰³ tempenv-18a097382f8c  /t/newdir  pipenv install dateparser
Creating a Pipfile for this project...
Installing dateparser...
Collecting dateparser
  Using cached dateparser-0.6.0-py2.py3-none-any.whl
Collecting python-dateutil (from dateparser)
  Using cached python_dateutil-2.6.1-py2.py3-none-any.whl
Collecting pytz (from dateparser)
  Using cached pytz-2017.2-py2.py3-none-any.whl
Collecting ruamel.yaml (from dateparser)
  Downloading ruamel.yaml-0.15.33-cp36-cp36m-manylinux1_x86_64.whl (557kB)
Collecting tzlocal (from dateparser)
Collecting regex (from dateparser)
Requirement already satisfied: six>=1.5 in /home/hawk/.virtualenvs/tempenv-18a097382f8c/lib/python3.6/site-packages (from python-dateutil->dateparser)
Installing collected packages: python-dateutil, pytz, ruamel.yaml, tzlocal, regex, dateparser
Successfully installed dateparser-0.6.0 python-dateutil-2.6.1 pytz-2017.2 regex-2017.7.28 ruamel.yaml-0.15.33 tzlocal-1.4

Adding dateparser to Pipfile's [packages]...
Locking [dev-packages] dependencies...
Locking [packages] dependencies...
Traceback (most recent call last):
  File "/home/hawk/.virtualenvs/tempenv-18a097382f8c/bin/pipenv", line 11, in <module>
    sys.exit(cli())
  File "/home/hawk/.virtualenvs/tempenv-18a097382f8c/lib/python3.6/site-packages/pipenv/vendor/click/core.py", line 722, in __call__
    return self.main(*args, **kwargs)
  File "/home/hawk/.virtualenvs/tempenv-18a097382f8c/lib/python3.6/site-packages/pipenv/vendor/click/core.py", line 697, in main
    rv = self.invoke(ctx)
  File "/home/hawk/.virtualenvs/tempenv-18a097382f8c/lib/python3.6/site-packages/pipenv/vendor/click/core.py", line 1066, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
  File "/home/hawk/.virtualenvs/tempenv-18a097382f8c/lib/python3.6/site-packages/pipenv/vendor/click/core.py", line 895, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/home/hawk/.virtualenvs/tempenv-18a097382f8c/lib/python3.6/site-packages/pipenv/vendor/click/core.py", line 535, in invoke
    return callback(*args, **kwargs)
  File "/home/hawk/.virtualenvs/tempenv-18a097382f8c/lib/python3.6/site-packages/pipenv/cli.py", line 989, in install
    do_lock(no_hashes=no_hashes)
  File "/home/hawk/.virtualenvs/tempenv-18a097382f8c/lib/python3.6/site-packages/pipenv/cli.py", line 531, in do_lock
    results = resolve_deps(deps, sources=project.sources, hashes=(not no_hashes))
  File "/home/hawk/.virtualenvs/tempenv-18a097382f8c/lib/python3.6/site-packages/pipenv/utils.py", line 72, in resolve_deps
    for release in r.json()['releases'][version]:
KeyError: '2017.7.28'
```


**Pip Resolver**

```python
>>> import pip
>>> import requests
>>> finder = pip.index.PackageFinder([], ['https://pypi.python.org/simple'], process_dependency_links=True, session=requests.Session())
>>> req = pip.req.InstallRequirement.from_line('regex>=2017.07.28')
>>> req
>>> regex_candidates = finder.find_all_candidates('regex')
>>> matches = list(req.specifier.filter([str(c.version) for c in regex_candidates]))
>>> candidates = [ireq for ireq in regex_candidates if str(ireq.version) in matches]
>>> print(candidates)
[<InstallationCandidate('regex', <Version('2017.7.28')>, <Link https://pypi.python.org/packages/d1/23/5fa829706ee1d4452552eb32e0bfc1039553e01f50a8754c6f7152e85c1b/regex-2017.07.28.tar.gz#md5=624168068a8a7728fb1f067db33fd5c4 (from https://pypi.python.org/simple/regex/)>)>]

>>> print(candidates[0].version)
2017.7.28
```

**PyPI Json API**

```json
{
	"info": {
		"package_url": "http://pypi.python.org/pypi/regex",
		"version": "2017.07.28",
		"release_url": "http://pypi.python.org/pypi/regex/2017.07.28",
		"name": "regex",
		"summary": "Alternative regular expression module, to replace re.",
	},
	"releases": {
		"2017.04.05": [{
				"md5_digest": "e621b9d5007e178559a33219e38655d7",
				"filename": "regex-2017.04.05-cp25-none-win32.whl",
			}
		],
		"2017.07.28": [{
				"md5_digest": "f7defbae447be68e9f19940630b9be8e",
				"filename": "regex-2017.07.28-cp25-none-win32.whl",
            }
        ]
    }
}
```


Discussion:

Resolving patch:
diff --git a/pipenv/utils.py b/pipenv/utils.py
--- a/pipenv/utils.py
+++ b/pipenv/utils.py
@@ -27,7 +27,7 @@ def shellquote(s):
 
 
 def clean_pkg_version(version):
-    return six.u(pep440_version(str(version))).replace('==', '')
+    return six.u(pep440_version(str(version).replace('==', '')))
 
 
 def resolve_deps(deps, sources=None, verbose=False, hashes=False):
@@ -69,14 +69,19 @@ def resolve_deps(deps, sources=None, verbose=False, hashes=False):
             try:
                 collected_hashes = []
                 r = requests.get('https://pypi.org/pypi/{0}/json'.format(name))
-                for release in r.json()['releases'][version]:
+                api_releases = r.json()['releases']
+                cleaned_releases = {}
+                for api_version, api_info in api_releases.items():
+                    cleaned_releases[clean_pkg_version(api_version)] = api_info
+
+                for release in cleaned_releases[version]:
                     collected_hashes.append(release['digests']['sha256'])
 
                 collected_hashes = ['sha256:' + s for s in collected_hashes]
 
                 # Collect un-collectable hashes.
                 if not collected_hashes:
-                    collected_hashes = list(resolver.resolve_hashes([result]).items()[0][1])
+                    collected_hashes = list(list(resolver.resolve_hashes([result]).items())[0][1])
 
                 results.append({'name': name, 'version': version, 'hashes': collected_hashes})
             except ValueError:
@@ -254,8 +259,9 @@ def is_vcs(pipfile_entry):
 
 
 def pep440_version(version):
-    # TODO: https://github.com/pypa/pip/blob/a9d56c7734fd465d01437d61f632749a293e7805/src/pip/_vendor/distlib/version.py#L184
-    return version.replace('.post', '-')
+    """Normalize version to PEP 440 standards"""
+    # use pip built in version parser
+    return str(pip.index.parse_version(version))
 
 
 def pep423_name(name):
@@ -346,14 +352,14 @@ def walk_up(bottom):
 
 
 def find_requirements(max_depth=3):
-        """Returns the path of a Pipfile in parent directories."""
-        i = 0
-        for c, d, f in walk_up(os.getcwd()):
-            i += 1
-
-            if i < max_depth:
-                if 'requirements.txt':
-                    r = os.path.join(c, 'requirements.txt')
-                    if os.path.isfile(r):
-                        return r
-        raise RuntimeError('No requirements.txt found!')
+    """Returns the path of a Pipfile in parent directories."""
+    i = 0
+    for c, d, f in walk_up(os.getcwd()):
+        i += 1
+
+        if i < max_depth:
+            if 'requirements.txt':
+                r = os.path.join(c, 'requirements.txt')
+                if os.path.isfile(r):
+                    return r
+    raise RuntimeError('No requirements.txt found!')


Test patch:
diff --git a/tests/test_pipenv.py b/tests/test_pipenv.py
--- a/tests/test_pipenv.py
+++ b/tests/test_pipenv.py
@@ -42,6 +42,7 @@ def test_cli_usage(self):
         assert delegator.run('pipenv --python python').return_code == 0
         assert delegator.run('pipenv install Werkzeug').return_code == 0
         assert delegator.run('pipenv install pytest --dev').return_code == 0
+        assert delegator.run('pipenv install regex').return_code == 0  # failing before
         assert delegator.run('pipenv install git+https://github.com/requests/requests.git@v2.18.4#egg=requests').return_code == 0
         assert delegator.run('pipenv lock').return_code == 0
 
@@ -151,7 +152,7 @@ def test_pipenv_uninstall(self):
         pipfile_output = delegator.run('cat Pipfile').out
         pipfile_list = pipfile_output.split('\n')
 
-        assert 'Werkzeug = "*"' in pipfile_list
+        assert 'werkzeug = "*"' in pipfile_list
         assert 'pytest = "*"' in pipfile_list
         assert '[packages]' in pipfile_list
         assert '[dev-packages]' in pipfile_list