Repository: timothycrosley/isort (python)

Issue:
Some builtins not properly placed on non-standard Python installations (conda)
Consider this file:

``` python
import thirdparty
import os
import ftplib
import ABC
import numbers
import colorama
```

On Windows with a **conda** Python 2.7 installation, `isort` produces this:

``` python
import os

import colorama

import ABC
import ftplib
import numbers
import thirdparty
```

(Note that `ftplib` and `numbers` were incorrectly placed in the "third party" section).

It seems the problem lies in the logic which decides which files are third-party (`isort.py:265`):

``` python
                elif 'python2' in prefix.lower() or 'python3' in prefix.lower():
                    return self.sections.STDLIB
```

Standard python is installed by default in `C:\Python27` or `C:\Python34` which explains why the above works in general, but in systems which have Python from anaconda or even Python installed in a non-standard location that logic does not work and the library is considered "thirdparty".

My Python is installed at `E:\Miniconda\python.exe` , so here's the path to `ftplib`:

```
$ python -c "import ftplib;print(ftplib.__file__)"
E:\Miniconda\lib\ftplib.pyc
```

A workaround is to add those builtin files in the `known_standard_library` config options.


Pull request:
Place modules found in sys.prefix/lib in stdlib section
This uses `sys.prefix` to decide if a file is from the stdlib instead of relying that `python2` or `python3` appear anywhere on the PATH.

This solves the problem of Python from official distribution installed in a non-standard location or other distributions (such as conda).

I don't know how I can write a test for the actual problem (Python installed in a non-standard location) because both AppVeyor and Travis have Python installed in a standard location. :sweat: 

Fix #464


Patch:
diff --git a/isort/isort.py b/isort/isort.py
index 01d0f9cc..e81463fc 100644
--- a/isort/isort.py
+++ b/isort/isort.py
@@ -254,15 +254,20 @@ class SortImports(object):
             paths += [path for path in glob('{0}/src/*'.format(virtual_env)) if os.path.isdir(path)]
             virtual_env_src = '{0}/src/'.format(virtual_env)
 
+        # handle case-insensitive paths on windows
+        stdlib_lib_prefix = os.path.normcase(get_stdlib_path())
+
         for prefix in paths:
             module_path = "/".join((prefix, module_name.replace(".", "/")))
             package_path = "/".join((prefix, module_name.split(".")[0]))
-            if (os.path.exists(module_path + ".py") or os.path.exists(module_path + ".so") or
-               (os.path.exists(package_path) and os.path.isdir(package_path))):
+            is_module = (exists_case_sensitive(module_path + ".py") or
+                         exists_case_sensitive(module_path + ".so"))
+            is_package = exists_case_sensitive(package_path) and os.path.isdir(package_path)
+            if is_module or is_package:
                 if ('site-packages' in prefix or 'dist-packages' in prefix or
                     (virtual_env and virtual_env_src in prefix)):
                     return self.sections.THIRDPARTY
-                elif 'python2' in prefix.lower() or 'python3' in prefix.lower():
+                elif os.path.normcase(prefix).startswith(stdlib_lib_prefix):
                     return self.sections.STDLIB
                 else:
                     return self.config['default_section']
@@ -894,3 +899,30 @@ def coding_check(fname, default='utf-8'):
                 break
 
     return coding
+
+
+def get_stdlib_path():
+    """Returns the path to the standard lib for the current path installation.
+
+    This function can be dropped and "sysconfig.get_paths()" used directly once Python 2.6 support is dropped.
+    """
+    if sys.version_info >= (2, 7):
+        import sysconfig
+        return sysconfig.get_paths()['stdlib']
+    else:
+        return os.path.join(sys.prefix, 'lib')
+
+
+def exists_case_sensitive(path):
+    """
+    Returns if the given path exists and also matches the case on Windows.
+
+    When finding files that can be imported, it is important for the cases to match because while
+    file os.path.exists("module.py") and os.path.exists("MODULE.py") both return True on Windows, Python
+    can only import using the case of the real file.
+    """
+    result = os.path.exists(path)
+    if sys.platform.startswith('win') and result:
+        directory, basename = os.path.split(path)
+        result = basename in os.listdir(directory)
+    return result


Test patch:
diff --git a/test_isort.py b/test_isort.py
index 52f91a45..e962cc2e 100644
--- a/test_isort.py
+++ b/test_isort.py
@@ -28,7 +28,7 @@ import os
 import shutil
 import tempfile
 
-from isort.isort import SortImports
+from isort.isort import exists_case_sensitive, SortImports
 from isort.pie_slice import *
 from isort.settings import WrapModes
 
@@ -1796,3 +1796,30 @@ def test_plone_style():
     options = {'force_single_line': True,
                'force_alphabetical_sort': True}
     assert SortImports(file_contents=test_input, **options).output == test_input
+
+
+def test_third_party_case_sensitive():
+    """Modules which match builtins by name but not on case should not be picked up on Windows."""
+    test_input = ("import thirdparty\n"
+                  "import os\n"
+                  "import ABC\n")
+
+    expected_output = ('import os\n'
+                       '\n'
+                       'import ABC\n'
+                       'import thirdparty\n')
+    assert SortImports(file_contents=test_input).output == expected_output
+
+
+def test_exists_case_sensitive_file(tmpdir):
+    """Test exists_case_sensitive function for a file."""
+    tmpdir.join('module.py').ensure(file=1)
+    assert exists_case_sensitive(str(tmpdir.join('module.py')))
+    assert not exists_case_sensitive(str(tmpdir.join('MODULE.py')))
+
+
+def test_exists_case_sensitive_directory(tmpdir):
+    """Test exists_case_sensitive function for a directory."""
+    tmpdir.join('pkg').ensure(dir=1)
+    assert exists_case_sensitive(str(tmpdir.join('pkg')))
+    assert not exists_case_sensitive(str(tmpdir.join('PKG')))