Repository: pypa/pip

Issue:
Traceback on invalid version spec
Invalid version specification should not give a trace back.

```
$ pip install pyprocessing=0.1.3.21
Exception:
Traceback (most recent call last):
  File "/lib/python2.7/site-packages/pip-1.1-py2.7.egg/pip/basecommand.py", line 104, in main
    status = self.run(options, args)
  File "/lib/python2.7/site-packages/pip-1.1-py2.7.egg/pip/commands/install.py", line 214, in run
    InstallRequirement.from_line(name, None))
  File "/lib/python2.7/site-packages/pip-1.1-py2.7.egg/pip/req.py", line 107, in from_line
    return cls(req, comes_from, url=url)
  File "/lib/python2.7/site-packages/pip-1.1-py2.7.egg/pip/req.py", line 40, in __init__
    req = pkg_resources.Requirement.parse(req)
  File "/lib/python2.7/site-packages/setuptools-0.6c11-py2.7.egg/pkg_resources.py", line 2510, in parse
    reqs = list(parse_requirements(s))
  File "/lib/python2.7/site-packages/setuptools-0.6c11-py2.7.egg/pkg_resources.py", line 2436, in parse_requirements
    line, p, specs = scan_list(VERSION,LINE_END,line,p,(1,2),"version spec")
  File "/lib/python2.7/site-packages/setuptools-0.6c11-py2.7.egg/pkg_resources.py", line 2404, in scan_list
    raise ValueError("Expected "+item_name+" in",line,"at",line[p:])
ValueError: ('Expected version spec in', 'pyprocessing=0.1.3.21', 'at', '=0.1.3.21')
```



Discussion:
Confirmed on develop

Will try get to tomorrow.

If you are at it, pyprocessing=0.3.11 could be a valid version spec. It's not a python code anyway - why constrain users?

The specification is defined by setuptools/easyinstall not pip and is `==` see

http://packages.python.org/distribute/setuptools.html#declaring-dependencies


Resolving patch:
diff --git a/pip/req/req_install.py b/pip/req/req_install.py
--- a/pip/req/req_install.py
+++ b/pip/req/req_install.py
@@ -15,6 +15,7 @@
 
 from pip._vendor import pkg_resources, six
 from pip._vendor.distlib.markers import interpret as markers_interpret
+from pip._vendor.packaging import specifiers
 from pip._vendor.six.moves import configparser
 
 import pip.wheel
@@ -44,6 +45,8 @@
 
 logger = logging.getLogger(__name__)
 
+operators = specifiers.Specifier._operators.keys()
+
 
 def _strip_extras(path):
     m = re.match(r'^(.+)(\[[^\]]+\])$', path)
@@ -65,7 +68,17 @@ def __init__(self, req, comes_from, source_dir=None, editable=False,
                  wheel_cache=None, constraint=False):
         self.extras = ()
         if isinstance(req, six.string_types):
-            req = pkg_resources.Requirement.parse(req)
+            try:
+                req = pkg_resources.Requirement.parse(req)
+            except pkg_resources.RequirementParseError:
+                if os.path.sep in req:
+                    add_msg = "It looks like a path. Does it exist ?"
+                elif '=' in req and not any(op in req for op in operators):
+                    add_msg = "= is not a valid operator. Did you mean == ?"
+                else:
+                    add_msg = traceback.format_exc()
+                raise InstallationError(
+                    "Invalid requirement: '%s'\n%s" % (req, add_msg))
             self.extras = req.extras
 
         self.req = req


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
@@ -8,9 +8,8 @@
 from mock import Mock, patch, mock_open
 from pip.commands.install import InstallCommand
 from pip.exceptions import (PreviousBuildDirError, InvalidWheelFilename,
-                            UnsupportedWheel)
+                            InstallationError, UnsupportedWheel, HashErrors)
 from pip.download import path_to_url, PipSession
-from pip.exceptions import HashErrors, InstallationError
 from pip.index import PackageFinder
 from pip.req import (InstallRequirement, RequirementSet, Requirements)
 from pip.req.req_file import process_line
@@ -447,6 +446,27 @@ def test_extras_for_editable_url_requirement(self):
         assert req.extras[0] == 'ex1'
         assert req.extras[1] == 'ex2'
 
+    def test_unexisting_path(self):
+        with pytest.raises(InstallationError) as e:
+            InstallRequirement.from_line('/this/path/does/not/exist')
+        err_msg = e.value.args[0]
+        assert "Invalid requirement" in err_msg
+        assert "It looks like a path. Does it exist ?" in err_msg
+
+    def test_single_equal_sign(self):
+        with pytest.raises(InstallationError) as e:
+            InstallRequirement.from_line('toto=42')
+        err_msg = e.value.args[0]
+        assert "Invalid requirement" in err_msg
+        assert "= is not a valid operator. Did you mean == ?" in err_msg
+
+    def test_traceback(self):
+        with pytest.raises(InstallationError) as e:
+            InstallRequirement.from_line('toto 42')
+        err_msg = e.value.args[0]
+        assert "Invalid requirement" in err_msg
+        assert "\nTraceback " in err_msg
+
 
 def test_requirements_data_structure_keeps_order():
     requirements = Requirements()
diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py
--- a/tests/unit/test_req_file.py
+++ b/tests/unit/test_req_file.py
@@ -7,7 +7,7 @@
 from pretend import stub
 
 import pip
-from pip.exceptions import (RequirementsFileParseError)
+from pip.exceptions import (InstallationError, RequirementsFileParseError)
 from pip.download import PipSession
 from pip.index import PackageFinder
 from pip.req.req_install import InstallRequirement
@@ -170,7 +170,7 @@ def test_parser_error(self):
 
     def test_only_one_req_per_line(self):
         # pkg_resources raises the ValueError
-        with pytest.raises(ValueError):
+        with pytest.raises(InstallationError):
             list(process_line("req1 req2", "file", 1))
 
     def test_yield_line_requirement(self):