Repository: python/mypy

Issue:
Stubgen stuck in an endless loop in 0.750
* Are you reporting a bug, or opening a feature request? **Bug**

Only reproducible on Windows b/c of different path separator. On a windows machine, issue:

```cmd
> mkdir foo bar
> touch foo\__init__.py foo\spam.py bar\__init__.py bar\eggs.py
> stubgen foo\spam.py bar\eggs.py
```
Result: `stubgen` never finishes, because caught in the endless while loop: `out\foo/` doesn't start with `out\bar/`, then doesn't start with `out/`, then with `/` repeatedly.
https://github.com/python/mypy/blob/4e021d908a8298c99c280f9cef20534c22c77d42/mypy/stubutil.py#L245-L249

Since `mypy` supports only Python 3.5 and onwards anyway - may I suggest using `pathlib` instead of string manipulation? With `pathlib`, the task of finding the common parent directory is a walk in the park and works on all OSes. For example, the `common_dir_prefix` method could look like this:

```py
from pathlib import Path

def common_dir_prefix(paths: List[str]) -> str:
    if not paths:
        return '.'
    cur = Path(paths[0])
    for parent in cur.parents:
        # if the parent is parent of all paths, we're good to go
        if all(parent in Path(path).parents for path in paths[1:]):
            return str(parent)
    return '.'
```
In addition, if an extensive path manipulation is done in `mypy` (didn't check that), I would also plead switching to `pathlib` exclusively.


Discussion:

Resolving patch:
diff --git a/mypy/stubutil.py b/mypy/stubutil.py
--- a/mypy/stubutil.py
+++ b/mypy/stubutil.py
@@ -247,11 +247,11 @@ def remove_misplaced_type_comments(source: Union[str, bytes]) -> Union[str, byte
 def common_dir_prefix(paths: List[str]) -> str:
     if not paths:
         return '.'
-    cur = os.path.dirname(paths[0])
+    cur = os.path.dirname(os.path.normpath(paths[0]))
     for path in paths[1:]:
         while True:
-            path = os.path.dirname(path)
-            if (cur + '/').startswith(path + '/'):
+            path = os.path.dirname(os.path.normpath(path))
+            if (cur + os.sep).startswith(path + os.sep):
                 cur = path
                 break
     return cur or '.'


Test patch:
diff --git a/mypy/test/teststubgen.py b/mypy/test/teststubgen.py
--- a/mypy/test/teststubgen.py
+++ b/mypy/test/teststubgen.py
@@ -445,7 +445,9 @@ def h():
 
         assert_equal(remove_misplaced_type_comments(original), dest)
 
-    def test_common_dir_prefix(self) -> None:
+    @unittest.skipIf(sys.platform == 'win32',
+                     'Tests building the paths common ancestor on *nix')
+    def test_common_dir_prefix_unix(self) -> None:
         assert common_dir_prefix([]) == '.'
         assert common_dir_prefix(['x.pyi']) == '.'
         assert common_dir_prefix(['./x.pyi']) == '.'
@@ -458,6 +460,26 @@ def test_common_dir_prefix(self) -> None:
         assert common_dir_prefix(['foo/x.pyi', 'foo/bar/zar/y.pyi']) == 'foo'
         assert common_dir_prefix(['foo/bar/zar/x.pyi', 'foo/bar/y.pyi']) == 'foo/bar'
         assert common_dir_prefix(['foo/bar/x.pyi', 'foo/bar/zar/y.pyi']) == 'foo/bar'
+        assert common_dir_prefix([r'foo/bar\x.pyi']) == 'foo'
+        assert common_dir_prefix([r'foo\bar/x.pyi']) == r'foo\bar'
+
+    @unittest.skipIf(sys.platform != 'win32',
+                     'Tests building the paths common ancestor on Windows')
+    def test_common_dir_prefix_win(self) -> None:
+        assert common_dir_prefix(['x.pyi']) == '.'
+        assert common_dir_prefix([r'.\x.pyi']) == '.'
+        assert common_dir_prefix([r'foo\bar\x.pyi']) == r'foo\bar'
+        assert common_dir_prefix([r'foo\bar\x.pyi',
+                                  r'foo\bar\y.pyi']) == r'foo\bar'
+        assert common_dir_prefix([r'foo\bar\x.pyi', r'foo\y.pyi']) == 'foo'
+        assert common_dir_prefix([r'foo\x.pyi', r'foo\bar\y.pyi']) == 'foo'
+        assert common_dir_prefix([r'foo\bar\zar\x.pyi', r'foo\y.pyi']) == 'foo'
+        assert common_dir_prefix([r'foo\x.pyi', r'foo\bar\zar\y.pyi']) == 'foo'
+        assert common_dir_prefix([r'foo\bar\zar\x.pyi', r'foo\bar\y.pyi']) == r'foo\bar'
+        assert common_dir_prefix([r'foo\bar\x.pyi', r'foo\bar\zar\y.pyi']) == r'foo\bar'
+        assert common_dir_prefix([r'foo/bar\x.pyi']) == r'foo\bar'
+        assert common_dir_prefix([r'foo\bar/x.pyi']) == r'foo\bar'
+        assert common_dir_prefix([r'foo/bar/x.pyi']) == r'foo\bar'
 
 
 class StubgenHelpersSuite(unittest.TestCase):