Repository: pypa/pip

Issue:
pip wheel should not create wheels that it will itself deem invalid
* Pip version: 9.0.1
* Python version: 3.5.2
* Operating System: Arch Linux

### Description:

`pip wheel` can create wheels that `pip install` will deem invalid.  This should not be the case.

### What I've run:

`setup.py`
```
from setuptools import setup
setup(name="foo", version="!invalid!")
```
then
```
$ pip wheel . && pip install *.whl
Processing /tmp/foo
Building wheels for collected packages: foo
  Running setup.py bdist_wheel for foo ... done
  Stored in directory: /tmp/foo
Successfully built foo
foo-_invalid_-py3-none-any.whl is not a valid wheel filename.
Exception information:
Traceback (most recent call last):
  File "/usr/lib/python3.5/site-packages/pip/basecommand.py", line 215, in main
    status = self.run(options, args)
  File "/usr/lib/python3.5/site-packages/pip/commands/install.py", line 312, in run
    wheel_cache
  File "/usr/lib/python3.5/site-packages/pip/basecommand.py", line 276, in populate_requirement_set
    wheel_cache=wheel_cache
  File "/usr/lib/python3.5/site-packages/pip/req/req_install.py", line 221, in from_line
    wheel = Wheel(link.filename)  # can raise InvalidWheelFilename
  File "/usr/lib/python3.5/site-packages/pip/wheel.py", line 631, in __init__
    "%s is not a valid wheel filename." % filename
pip.exceptions.InvalidWheelFilename: foo-_invalid_-py3-none-any.whl is not a valid wheel filename.
```
`pip wheel` doesn't even bother to warn on the version name.

As a side note, note that "interestingly", `pip install .` (installation from the directory) works just fine.
As a further note, I think that internal exceptions from pip should, by default, not display a traceback.



Discussion:
Indeed the https://github.com/pypa/pip/blob/b559221/pip/wheel.py#L584-L588 regex seems a little too strict compared to the https://www.python.org/dev/peps/pep-0427/ requirements.
The real issue being that pip should actually install this valid wheel (with a invalid/non PEP-440 version number).
I don't have a preference, other than consistency.

Resolving patch:
diff --git a/pip/wheel.py b/pip/wheel.py
--- a/pip/wheel.py
+++ b/pip/wheel.py
@@ -187,7 +187,7 @@ def fix_script(path):
             script.write(rest)
         return True
 
-dist_info_re = re.compile(r"""^(?P<namever>(?P<name>.+?)(-(?P<ver>\d.+?))?)
+dist_info_re = re.compile(r"""^(?P<namever>(?P<name>.+?)(-(?P<ver>.+?))?)
                                 \.dist-info$""", re.VERBOSE)
 
 
@@ -582,7 +582,7 @@ class Wheel(object):
     # TODO: maybe move the install code into this class
 
     wheel_file_re = re.compile(
-        r"""^(?P<namever>(?P<name>.+?)-(?P<ver>\d.*?))
+        r"""^(?P<namever>(?P<name>.+?)-(?P<ver>.*?))
         ((-(?P<build>\d.*?))?-(?P<pyver>.+?)-(?P<abi>.+?)-(?P<plat>.+?)
         \.whl|\.dist-info)$""",
         re.VERBOSE


Test patch:
diff --git a/tests/data/packages/plat_wheel-_invalidversion_/plat_wheel-_invalidversion_.dist-info/WHEEL b/tests/data/packages/plat_wheel-_invalidversion_/plat_wheel-_invalidversion_.dist-info/WHEEL
new file mode 100644
--- /dev/null
+++ b/tests/data/packages/plat_wheel-_invalidversion_/plat_wheel-_invalidversion_.dist-info/WHEEL
@@ -0,0 +1,4 @@
+Wheel-Version: 0.1
+Packager: bdist_wheel
+Root-Is-Purelib: false
+
diff --git a/tests/data/packages/pure_wheel-_invalidversion_/pure_wheel-_invalidversion_.dist-info/WHEEL b/tests/data/packages/pure_wheel-_invalidversion_/pure_wheel-_invalidversion_.dist-info/WHEEL
new file mode 100644
--- /dev/null
+++ b/tests/data/packages/pure_wheel-_invalidversion_/pure_wheel-_invalidversion_.dist-info/WHEEL
@@ -0,0 +1,4 @@
+Wheel-Version: 0.1
+Packager: bdist_wheel
+Root-Is-Purelib: true
+
diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py
--- a/tests/unit/test_wheel.py
+++ b/tests/unit/test_wheel.py
@@ -100,6 +100,10 @@ def test_single_digit_version(self):
         w = wheel.Wheel('simple-1-py2-none-any.whl')
         assert w.version == '1'
 
+    def test_non_pep440_version(self):
+        w = wheel.Wheel('simple-_invalid_-py2-none-any.whl')
+        assert w.version == '-invalid-'
+
     def test_missing_version_raises(self):
         with pytest.raises(InvalidWheelFilename):
             wheel.Wheel('Cython-cp27-none-linux_x86_64.whl')
@@ -257,6 +261,10 @@ def test_purelib_platlib(self, data):
         packages = [
             ("pure_wheel", data.packages.join("pure_wheel-1.7"), True),
             ("plat_wheel", data.packages.join("plat_wheel-1.7"), False),
+            ("pure_wheel", data.packages.join(
+                "pure_wheel-_invalidversion_"), True),
+            ("plat_wheel", data.packages.join(
+                "plat_wheel-_invalidversion_"), False),
         ]
 
         for name, path, expected in packages: