Repository: pypa/pip

Issue:
Require hashes' suggested changes are not correct for URL requirements
When using pip v8's new hash checking feature (eg by forcing it on with `--require-hashes`), missing hashes for packages are reported like so:

```
Hashes are required in --require-hashes mode, but they are missing from some requirements. Here is a list of those requirements along with the hashes their downloaded archives actually had. Add lines like these to your requirements files to prevent tampering. (If you did not enable --require-hashes manually, note that it turns on automatically when any package has a hash.)
    gunicorn==19.4.3 --hash=sha256:ddd0302aecac53d0b540909efef51d3725457b57bf98d6ee49da4a2c5029cd8a
    wsgi-sslify==1.0.1 --hash=sha256:cde368fda0fb9958dd58bc2cb955d0bf3df1b79c132d97cee90be5fda34a5089
    whitenoise==2.0.6 --hash=sha256:826ffe5d608c9dc8daebef1b0b43d01f7958f17c2fce36e75c80e26160172c4f
```

However for requirements file lines that are just URLs, eg:

```
https://github.com/benoitc/gunicorn/archive/3ccdafbb62db9b22482e2cc58fe7f066a8faa31a.zip#egg=gunicorn==19.4.5
```

The help text says:

```
    gunicorn==19.4.5 --hash=sha256:914985a0b4dd9088b3d82ccad098e9ed88c5c4f79e3ed73078deb8e57bdc7c5a
```

Rather than:

```
    https://github.com/benoitc/gunicorn/archive/3ccdafbb62db9b22482e2cc58fe7f066a8faa31a.zip#egg=gunicorn==19.4.5 --hash=sha256:914985a0b4dd9088b3d82ccad098e9ed88c5c4f79e3ed73078deb8e57bdc7c5a
```

In addition, if you accidentally omit the `#egg=` suffix from the requirements file line, eg:

```
https://github.com/benoitc/gunicorn/archive/3ccdafbb62db9b22482e2cc58fe7f066a8faa31a.zip
```

...the package is reported as unknown, eg:

```
    unknown package --hash=sha256:914985a0b4dd9088b3d82ccad098e9ed88c5c4f79e3ed73078deb8e57bdc7c5a
```

These aren't super-urgent, since they (a) only affect people trying out this new optional mode, and (b) are pretty easy to figure out, but thought worth filing regardless :-)

CC @erikrose 



Discussion:

Resolving patch:
diff --git a/pip/exceptions.py b/pip/exceptions.py
--- a/pip/exceptions.py
+++ b/pip/exceptions.py
@@ -160,13 +160,16 @@ def __init__(self, gotten_hash):
     def body(self):
         from pip.utils.hashes import FAVORITE_HASH  # Dodge circular import.
 
-        package_name = (self.req.req if self.req and
-                        # In case someone feeds something
-                        # downright stupid to
-                        # InstallRequirement's constructor:
-                        getattr(self.req, 'req', None)
-                        else 'unknown package')
-        return '    %s --hash=%s:%s' % (package_name,
+        package = None
+        if self.req:
+            # In the case of URL-based requirements, display the original URL
+            # seen in the requirements file rather than the package name,
+            # so the output can be directly copied into the requirements file.
+            package = (self.req.original_link if self.req.original_link
+                       # In case someone feeds something downright stupid
+                       # to InstallRequirement's constructor.
+                       else getattr(self.req, 'req', None))
+        return '    %s --hash=%s:%s' % (package or 'unknown package',
                                         FAVORITE_HASH,
                                         self.gotten_hash)
 


Test patch:
diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py
--- a/tests/unit/test_req.py
+++ b/tests/unit/test_req.py
@@ -93,6 +93,12 @@ def test_missing_hash_checking(self, data):
             list(process_line('https://pypi.python.org/packages/source/m/more-'
                               'itertools/more-itertools-1.0.tar.gz#md5=b21850c'
                               '3cfa7efbb70fd662ab5413bdd', 'file', 3))[0])
+        # The error text should list this as a URL and not `peep==3.1.1`:
+        reqset.add_requirement(
+            list(process_line('https://pypi.python.org/packages/source/p/peep/'
+                              'peep-3.1.1.tar.gz',
+                              'file',
+                              4))[0])
         finder = PackageFinder([],
                                ['https://pypi.python.org/simple'],
                                session=PipSession())
@@ -100,6 +106,8 @@ def test_missing_hash_checking(self, data):
             HashErrors,
             r'Hashes are required in --require-hashes mode, but they are '
             r'missing .*\n'
+            r'    https://pypi\.python\.org/packages/source/p/peep/peep'
+            r'-3\.1\.1\.tar\.gz --hash=sha256:[0-9a-f]+\n'
             r'    blessings==1.0 --hash=sha256:[0-9a-f]+\n'
             r'THESE PACKAGES DO NOT MATCH THE HASHES.*\n'
             r'    tracefront==0.1 .*:\n'