Repository: pypa/pip

Issue:
pip doesn't accept forward slashes as path separators under Windows
- Pip version: pip 8.1.2
- Python version: 3.5.2
- Operating System: Windows 10
### Description:

It would be nice if pip would accept forward slashes as path separators on Windows when installing from a local directory. This is useful for cross-platform scripts (tox-dev/tox#384).
### What I've run:

```
pip install tests_regression/sphinx
```

```
Invalid requirement: 'tests_regression/sphinx'
Traceback (most recent call last):
  File "c:\program files\python35\lib\site-packages\pip\_vendor\packaging\requirements.py", line 92, in __init__
    req = REQUIREMENT.parseString(requirement_string)
  File "c:\program files\python35\lib\site-packages\pip\_vendor\pyparsing.py", line 1172, in parseString
    raise exc
  File "c:\program files\python35\lib\site-packages\pip\_vendor\pyparsing.py", line 1162, in parseString
    loc, tokens = self._parse( instring, 0 )
  File "c:\program files\python35\lib\site-packages\pip\_vendor\pyparsing.py", line 1028, in _parseNoCache
    loc,tokens = self.parseImpl( instring, preloc, doActions )
  File "c:\program files\python35\lib\site-packages\pip\_vendor\pyparsing.py", line 2462, in parseImpl
    loc, exprtokens = e._parse( instring, loc, doActions )
  File "c:\program files\python35\lib\site-packages\pip\_vendor\pyparsing.py", line 1032, in _parseNoCache
    loc,tokens = self.parseImpl( instring, preloc, doActions )
  File "c:\program files\python35\lib\site-packages\pip\_vendor\pyparsing.py", line 2265, in parseImpl
    raise ParseException(instring, loc, self.errmsg, self)
pip._vendor.pyparsing.ParseException: Expected stringEnd (at char 16), (line:1, col:17)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "c:\program files\python35\lib\site-packages\pip\req\req_install.py", line 78, in __init__
    req = Requirement(req)
  File "c:\program files\python35\lib\site-packages\pip\_vendor\packaging\requirements.py", line 96, in __init__
    requirement_string[e.loc:e.loc + 8]))
pip._vendor.packaging.requirements.InvalidRequirement: Invalid requirement, parse error at "'/sphinx'"
```

Using the Windows-native backslash does work as expected.



Discussion:
I have tried to to fix this issue, but I couldn't find where I could apply `os.path.normpath` to the path.

It is interesting to note that passing a forward-slashed path to a wheel file causes no problems. So the issue is limited to paths specifying a project directory containing a `setup.py`.

If you use `pip install ./tests_regression/sphinx` instead, it should work, cf https://github.com/pypa/pip/blob/8.1.2/pip/req/req_install.py#L186-L187

Thanks @xavfernandez! That basically fixes it for my use case.

Good news.
I'm wondering if we should change the `os.path.sep in name` by `os.path.sep in os.path.normcase(name)` ? Or something else ?
Any opinion @pfmoore ?

I don't really understand what that test is for. It seems to be trying to apply a heuristic of "must have a path separator or an initial '.'" to avoid having a directory hide a genuine project name. Maybe the test should explicitly be "could it be a valid project name?" - isn't there a utility function anywhere that validates a project name according to https://www.python.org/dev/peps/pep-0426/#name?

Or we could check for `os.sep` or `os.altsep` explicitly. That's probably clearer than using `normcase` purely for its behaviour of replacing `/` with `\`.

I guess it is for the case where you `pip install package_name` with a subdirectory called `package_name`.
You'd want:
- `pip install package_name` to install the PyPI package
- `pip install ./package_name` to install the directory

Agreed. So let's either check "does it look like a valid package name" or just add `os.altsep` to the checks. (First option feels more "correct" to me, but second is simpler to do).

The issue with the first option is that it would changes the current behavior/logic.

`os.altsep` seems indeed simpler and cleaner than my `os.path.normpath`.
Let's go with `os.altsep` then ?

What's the status of this issue?
I have a Windows system and some free time.

Please guide me to the required files which have to be changed.
@xavfernandez What's the status of this issue?
I have a Windows system and some free time.

Please guide me to the required files which have to be changed.
Hey @Riyuzakii! Sorry for the delay!

Would you be willing to write and submit a patch for this?

The relevant lines would be in pip/req/req_install.py [here](https://github.com/pypa/pip/blob/master/pip/req/req_install.py#L206).

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
@@ -194,9 +194,12 @@ def from_line(
             link = Link(name)
         else:
             p, extras = _strip_extras(path)
-            if (os.path.isdir(p) and
-                    (os.path.sep in name or name.startswith('.'))):
-
+            looks_like_dir = os.path.isdir(p) and (
+                os.path.sep in name or
+                (os.path.altsep is not None and os.path.altsep in name) or
+                name.startswith('.')
+            )
+            if looks_like_dir:
                 if not is_installable_dir(p):
                     raise InstallationError(
                         "Directory %r is not installable. File 'setup.py' "


Test patch:
diff --git a/tests/unit/test_req_install.py b/tests/unit/test_req_install.py
--- a/tests/unit/test_req_install.py
+++ b/tests/unit/test_req_install.py
@@ -26,3 +26,18 @@ def test_tmp_build_directory(self):
             assert os.path.dirname(tmp_build_dir) == os.path.dirname(tmp_dir)
         os.rmdir(tmp_dir)
         assert not os.path.exists(tmp_dir)
+
+    def test_forward_slash_results_in_a_link(self, tmpdir):
+        install_dir = tmpdir / "foo" / "bar"
+
+        # Just create a file for letting the logic work
+        setup_py_path = install_dir / "setup.py"
+        os.makedirs(str(install_dir))
+        with open(setup_py_path, 'w') as f:
+            f.write('')
+
+        requirement = InstallRequirement.from_line(
+            str(install_dir).replace(os.sep, os.altsep or os.sep)
+        )
+
+        assert requirement.link is not None