summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--README.rst16
-rw-r--r--isort/finders.py325
-rw-r--r--isort/isort.py120
-rw-r--r--isort/utils.py30
-rwxr-xr-xsetup.py4
-rw-r--r--test_isort.py80
-rw-r--r--tox.ini6
8 files changed, 463 insertions, 120 deletions
diff --git a/.gitignore b/.gitignore
index 24a8bb32..11ade0b1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
*.py[cod]
+__pycache__
.DS_Store
# C extensions
*.so
@@ -25,6 +26,7 @@ npm-debug.log
# Unit test / coverage reports
.coverage
+.pytest_cache
.tox
nosetests.xml
htmlcov
diff --git a/README.rst b/README.rst
index 1a19d94e..d4646f9a 100644
--- a/README.rst
+++ b/README.rst
@@ -86,11 +86,23 @@ Installing isort is as simple as:
pip install isort
-or if you prefer
+Install isort with requirements.txt support:
.. code-block:: bash
- easy_install isort
+ pip install isort[requirements]
+
+Install isort with Pipfile support:
+
+.. code-block:: bash
+
+ pip install isort[pipfile]
+
+Install isort with both formats support:
+
+.. code-block:: bash
+
+ pip install isort[requirements,pipfile]
Using isort
===========
diff --git a/isort/finders.py b/isort/finders.py
new file mode 100644
index 00000000..d3a3c074
--- /dev/null
+++ b/isort/finders.py
@@ -0,0 +1,325 @@
+"""Finders try to find right section for passed module name
+"""
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+import inspect
+import os
+import os.path
+import re
+import sys
+import sysconfig
+from fnmatch import fnmatch
+from glob import glob
+
+from .pie_slice import PY2
+from .utils import chdir, exists_case_sensitive
+
+try:
+ from pipreqs import pipreqs
+except ImportError:
+ pipreqs = None
+
+try:
+ # pip>=10
+ from pip._internal.download import PipSession
+ from pip._internal.req import parse_requirements
+except ImportError:
+ try:
+ from pip.download import PipSession
+ from pip.req import parse_requirements
+ except ImportError:
+ parse_requirements = None
+
+try:
+ from requirementslib import Pipfile
+except ImportError:
+ Pipfile = None
+
+
+KNOWN_SECTION_MAPPING = {
+ 'STDLIB': 'STANDARD_LIBRARY',
+ 'FUTURE': 'FUTURE_LIBRARY',
+ 'FIRSTPARTY': 'FIRST_PARTY',
+ 'THIRDPARTY': 'THIRD_PARTY',
+}
+
+
+class BaseFinder(object):
+ def __init__(self, config, sections):
+ self.config = config
+ self.sections = sections
+
+
+class ForcedSeparateFinder(BaseFinder):
+ def find(self, module_name):
+ for forced_separate in self.config['forced_separate']:
+ # Ensure all forced_separate patterns will match to end of string
+ path_glob = forced_separate
+ if not forced_separate.endswith('*'):
+ path_glob = '%s*' % forced_separate
+
+ if fnmatch(module_name, path_glob) or fnmatch(module_name, '.' + path_glob):
+ return forced_separate
+
+
+class LocalFinder(BaseFinder):
+ def find(self, module_name):
+ if module_name.startswith("."):
+ return self.sections.LOCALFOLDER
+
+
+class KnownPatternFinder(BaseFinder):
+ def __init__(self, config, sections):
+ super(KnownPatternFinder, self).__init__(config, sections)
+
+ self.known_patterns = []
+ for placement in reversed(self.sections):
+ known_placement = KNOWN_SECTION_MAPPING.get(placement, placement)
+ config_key = 'known_{0}'.format(known_placement.lower())
+ known_patterns = self.config.get(config_key, [])
+ known_patterns = [
+ pattern
+ for known_pattern in known_patterns
+ for pattern in self._parse_known_pattern(known_pattern)
+ ]
+ for known_pattern in known_patterns:
+ regexp = '^' + known_pattern.replace('*', '.*').replace('?', '.?') + '$'
+ self.known_patterns.append((re.compile(regexp), placement))
+
+ @staticmethod
+ def _is_package(path):
+ """
+ Evaluates if path is a python package
+ """
+ if PY2:
+ return os.path.exists(os.path.join(path, '__init__.py'))
+ else:
+ return os.path.isdir(path)
+
+ def _parse_known_pattern(self, pattern):
+ """
+ Expand pattern if identified as a directory and return found sub packages
+ """
+ if pattern.endswith(os.path.sep):
+ patterns = [
+ filename
+ for filename in os.listdir(pattern)
+ if self._is_package(os.path.join(pattern, filename))
+ ]
+ else:
+ patterns = [pattern]
+
+ return patterns
+
+ def find(self, module_name):
+ # Try to find most specific placement instruction match (if any)
+ parts = module_name.split('.')
+ module_names_to_check = ('.'.join(parts[:first_k]) for first_k in range(len(parts), 0, -1))
+ for module_name_to_check in module_names_to_check:
+ for pattern, placement in self.known_patterns:
+ if pattern.match(module_name_to_check):
+ return placement
+
+
+class PathFinder(BaseFinder):
+ def __init__(self, config, sections):
+ super(PathFinder, self).__init__(config, sections)
+
+ # Use a copy of sys.path to avoid any unintended modifications
+ # to it - e.g. `+=` used below will change paths in place and
+ # if not copied, consequently sys.path, which will grow unbounded
+ # with duplicates on every call to this method.
+ self.paths = list(sys.path)
+ # restore the original import path (i.e. not the path to bin/isort)
+ self.paths[0] = os.getcwd()
+
+ # virtual env
+ self.virtual_env = self.config.get('virtual_env') or os.environ.get('VIRTUAL_ENV')
+ self.virtual_env_src = False
+ if self.virtual_env:
+ self.virtual_env_src = '{0}/src/'.format(self.virtual_env)
+ for path in glob('{0}/lib/python*/site-packages'.format(self.virtual_env)):
+ if path not in self.paths:
+ self.paths.append(path)
+ for path in glob('{0}/src/*'.format(self.virtual_env)):
+ if os.path.isdir(path):
+ self.paths.append(path)
+
+ # handle case-insensitive paths on windows
+ self.stdlib_lib_prefix = os.path.normcase(sysconfig.get_paths()['stdlib'])
+
+ def find(self, module_name):
+ for prefix in self.paths:
+ package_path = "/".join((prefix, module_name.split(".")[0]))
+ is_module = (exists_case_sensitive(package_path + ".py") or
+ exists_case_sensitive(package_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:
+ return self.sections.THIRDPARTY
+ if 'dist-packages' in prefix:
+ return self.sections.THIRDPARTY
+ if self.virtual_env and self.virtual_env_src in prefix:
+ return self.sections.THIRDPARTY
+ if os.path.normcase(prefix).startswith(self.stdlib_lib_prefix):
+ return self.sections.STDLIB
+ return self.config['default_section']
+
+
+class ReqsBaseFinder(BaseFinder):
+ def __init__(self, config, sections, path='.'):
+ super(ReqsBaseFinder, self).__init__(config, sections)
+ self.path = path
+ if self.enabled:
+ self.mapping = self._load_mapping()
+ self.names = self._load_names()
+
+ @staticmethod
+ def _load_mapping():
+ """Return list of mappings `package_name -> module_name`
+
+ Example:
+ django-haystack -> haystack
+ """
+ if not pipreqs:
+ return
+ path = os.path.dirname(inspect.getfile(pipreqs))
+ path = os.path.join(path, 'mapping')
+ with open(path, "r") as f:
+ # pypi_name: import_name
+ return dict(line.strip().split(":")[::-1] for line in f)
+
+ def _load_names(self):
+ """Return list of thirdparty modules from requirements
+ """
+ names = []
+ for path in self._get_files():
+ for name in self._get_names(path):
+ names.append(self._normalize_name(name))
+ return names
+
+ @staticmethod
+ def _get_parents(path):
+ prev = ''
+ while path != prev:
+ prev = path
+ yield path
+ path = os.path.dirname(path)
+
+ def _get_files(self):
+ """Return paths to all requirements files
+ """
+ path = os.path.abspath(self.path)
+ if os.path.isfile(path):
+ path = os.path.dirname(path)
+
+ for path in self._get_parents(path):
+ for file_path in self._get_files_from_dir(path):
+ yield file_path
+
+ def _normalize_name(self, name):
+ """Convert package name to module name
+
+ Examples:
+ Django -> django
+ django-haystack -> haystack
+ Flask-RESTFul -> flask_restful
+ """
+ if self.mapping:
+ name = self.mapping.get(name, name)
+ return name.lower().replace('-', '_')
+
+ def find(self, module_name):
+ # required lib not installed yet
+ if not self.enabled:
+ return
+
+ module_name, _sep, _submodules = module_name.partition('.')
+ module_name = module_name.lower()
+ if not module_name:
+ return
+
+ for name in self.names:
+ if module_name == name:
+ return self.sections.THIRDPARTY
+
+
+class RequirementsFinder(ReqsBaseFinder):
+ exts = ('.txt', '.in')
+ enabled = bool(parse_requirements)
+
+ def _get_files_from_dir(self, path):
+ """Return paths to requirements files from passed dir.
+ """
+ for fname in os.listdir(path):
+ if 'requirements' not in fname:
+ continue
+ full_path = os.path.join(path, fname)
+
+ # *requirements*/*.{txt,in}
+ if os.path.isdir(full_path):
+ for subfile_name in os.listdir(path):
+ for ext in self.exts:
+ if subfile_name.endswith(ext):
+ yield os.path.join(path, subfile_name)
+ continue
+
+ # *requirements*.{txt,in}
+ if os.path.isfile(full_path):
+ for ext in self.exts:
+ if fname.endswith(ext):
+ yield full_path
+ break
+
+ def _get_names(self, path):
+ """Load required packages from path to requirements file
+ """
+ with chdir(os.path.dirname(path)):
+ requirements = parse_requirements(path, session=PipSession())
+ for req in requirements:
+ if req.name:
+ yield req.name
+
+
+class PipfileFinder(ReqsBaseFinder):
+ enabled = bool(Pipfile)
+
+ def _get_names(self, path):
+ with chdir(path):
+ project = Pipfile.load(path)
+ sections = project.get_sections()
+ for section in sections.values():
+ for name in section:
+ yield name
+
+ def _get_files_from_dir(self, path):
+ if 'Pipfile' in os.listdir(path):
+ yield path
+
+
+class DefaultFinder(BaseFinder):
+ def find(self, module_name):
+ return self.config['default_section']
+
+
+class FindersManager(object):
+ finders = (
+ ForcedSeparateFinder,
+ LocalFinder,
+ KnownPatternFinder,
+ PathFinder,
+ PipfileFinder,
+ RequirementsFinder,
+ DefaultFinder,
+ )
+
+ def __init__(self, config, sections, finders=None):
+ if finders is not None:
+ self.finders = finders
+ self.finders = tuple(finder(config, sections) for finder in self.finders)
+
+ def find(self, module_name):
+ for finder in self.finders:
+ section = finder.find(module_name)
+ if section is not None:
+ return section
diff --git a/isort/isort.py b/isort/isort.py
index e3594716..30c263b9 100644
--- a/isort/isort.py
+++ b/isort/isort.py
@@ -32,23 +32,14 @@ import itertools
import os
import re
import sys
-import sysconfig
from collections import OrderedDict, namedtuple
from datetime import datetime
from difflib import unified_diff
-from fnmatch import fnmatch
-from glob import glob
from . import settings
+from .finders import FindersManager
from .natural import nsorted
-from .pie_slice import OrderedSet, input, itemsview, PY2
-
-KNOWN_SECTION_MAPPING = {
- 'STDLIB': 'STANDARD_LIBRARY',
- 'FUTURE': 'FUTURE_LIBRARY',
- 'FIRSTPARTY': 'FIRST_PARTY',
- 'THIRDPARTY': 'THIRD_PARTY',
-}
+from .pie_slice import OrderedSet, input, itemsview
class SortImports(object):
@@ -147,19 +138,7 @@ class SortImports(object):
for section in itertools.chain(self.sections, self.config['forced_separate']):
self.imports[section] = {'straight': OrderedSet(), 'from': OrderedDict()}
- self.known_patterns = []
- for placement in reversed(self.sections):
- known_placement = KNOWN_SECTION_MAPPING.get(placement, placement)
- config_key = 'known_{0}'.format(known_placement.lower())
- known_patterns = self.config.get(config_key, [])
- known_patterns = [
- pattern
- for known_pattern in known_patterns
- for pattern in self._parse_known_pattern(known_pattern)
- ]
- for known_pattern in known_patterns:
- self.known_patterns.append((re.compile('^' + known_pattern.replace('*', '.*').replace('?', '.?') + '$'),
- placement))
+ self.finder = FindersManager(config=self.config, sections=self.sections)
self.index = 0
self.import_index = -1
@@ -222,30 +201,6 @@ class SortImports(object):
print("Fixing {0}".format(self.file_path))
output_file.write(self.output)
- def _is_package(self, path):
- """
- Evaluates if path is a python package
- """
- if PY2:
- return os.path.exists(os.path.join(path, '__init__.py'))
- else:
- return os.path.isdir(path)
-
- def _parse_known_pattern(self, pattern):
- """
- Expand pattern if identified as a directory and return found sub packages
- """
- if pattern.endswith(os.path.sep):
- patterns = [
- filename
- for filename in os.listdir(pattern)
- if self._is_package(os.path.join(pattern, filename))
- ]
- else:
- patterns = [pattern]
-
- return patterns
-
def _show_diff(self, file_contents):
for line in unified_diff(
file_contents.splitlines(1),
@@ -272,59 +227,7 @@ class SortImports(object):
if it can't determine - it assumes it is project code
"""
- for forced_separate in self.config['forced_separate']:
- # Ensure all forced_separate patterns will match to end of string
- path_glob = forced_separate
- if not forced_separate.endswith('*'):
- path_glob = '%s*' % forced_separate
-
- if fnmatch(module_name, path_glob) or fnmatch(module_name, '.' + path_glob):
- return forced_separate
-
- if module_name.startswith("."):
- return self.sections.LOCALFOLDER
-
- # Try to find most specific placement instruction match (if any)
- parts = module_name.split('.')
- module_names_to_check = ['.'.join(parts[:first_k]) for first_k in range(len(parts), 0, -1)]
- for module_name_to_check in module_names_to_check:
- for pattern, placement in self.known_patterns:
- if pattern.match(module_name_to_check):
- return placement
-
- # Use a copy of sys.path to avoid any unintended modifications
- # to it - e.g. `+=` used below will change paths in place and
- # if not copied, consequently sys.path, which will grow unbounded
- # with duplicates on every call to this method.
- paths = list(sys.path)
- # restore the original import path (i.e. not the path to bin/isort)
- paths[0] = os.getcwd()
- virtual_env = self.config.get('virtual_env') or os.environ.get('VIRTUAL_ENV')
- virtual_env_src = False
- if virtual_env:
- paths += [path for path in glob('{0}/lib/python*/site-packages'.format(virtual_env))
- if path not in paths]
- 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(sysconfig.get_paths()['stdlib'])
-
- for prefix in paths:
- package_path = "/".join((prefix, module_name.split(".")[0]))
- is_module = (exists_case_sensitive(package_path + ".py") or
- exists_case_sensitive(package_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 os.path.normcase(prefix).startswith(stdlib_lib_prefix):
- return self.sections.STDLIB
- else:
- return self.config['default_section']
-
- return self.config['default_section']
+ return self.finder.find(module_name)
def _get_line(self):
"""Returns the current line from the file while incrementing the index."""
@@ -1065,18 +968,3 @@ def coding_check(fname, default='utf-8'):
break
return coding
-
-
-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') or sys.platform == 'darwin') and result:
- directory, basename = os.path.split(path)
- result = basename in os.listdir(directory)
- return result
diff --git a/isort/utils.py b/isort/utils.py
new file mode 100644
index 00000000..9206c88e
--- /dev/null
+++ b/isort/utils.py
@@ -0,0 +1,30 @@
+import os
+import sys
+from contextlib import contextmanager
+
+
+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') or sys.platform == 'darwin') and result:
+ directory, basename = os.path.split(path)
+ result = basename in os.listdir(directory)
+ return result
+
+
+@contextmanager
+def chdir(path):
+ """Context manager for changing dir and restoring previous workdir after exit.
+ """
+ curdir = os.getcwd()
+ os.chdir(path)
+ try:
+ yield
+ finally:
+ os.chdir(curdir)
diff --git a/setup.py b/setup.py
index 341e42eb..9332dae5 100755
--- a/setup.py
+++ b/setup.py
@@ -54,6 +54,10 @@ setup(name='isort',
'pylama.linter': ['isort = isort.pylama_isort:Linter'],
},
packages=['isort'],
+ extras_require={
+ 'requirements': ['pip', 'pipreqs'],
+ 'pipfile': ['pipreqs', 'requirementslib'],
+ },
install_requires=['futures; python_version < "3.2"'],
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*",
cmdclass={'test': PyTest},
diff --git a/test_isort.py b/test_isort.py
index 0bffe9f1..909d83f2 100644
--- a/test_isort.py
+++ b/test_isort.py
@@ -31,7 +31,9 @@ import shutil
import sys
import tempfile
-from isort.isort import SortImports, exists_case_sensitive
+from isort import finders
+from isort.isort import SortImports
+from isort.utils import exists_case_sensitive
from isort.main import is_python_file
from isort.pie_slice import PY2
from isort.settings import WrapModes
@@ -2465,3 +2467,79 @@ def test_new_lines_are_preserved():
with io.open(n_newline.name, newline='') as n_newline_file:
n_newline_contents = n_newline_file.read()
assert n_newline_contents == 'import os\nimport sys\n'
+
+
+def test_requirements_finder(tmpdir):
+ subdir = tmpdir.mkdir('subdir').join("lol.txt")
+ subdir.write("flask")
+ req_file = tmpdir.join('requirements.txt')
+ req_file.write(
+ "Django==1.11\n"
+ "-e git+https://github.com/orsinium/deal.git#egg=deal\n"
+ )
+ si = SortImports(file_contents="")
+ for path in (str(tmpdir), str(subdir)):
+ finder = finders.RequirementsFinder(
+ config=si.config,
+ sections=si.sections,
+ path=path
+ )
+
+ files = list(finder._get_files())
+ assert len(files) == 1 # file finding
+ assert files[0].endswith('requirements.txt') # file finding
+ assert list(finder._get_names(str(req_file))) == ['Django', 'deal'] # file parsing
+
+ assert finder.find("django") == si.sections.THIRDPARTY # package in reqs
+ assert finder.find("flask") is None # package not in reqs
+ assert finder.find("deal") == si.sections.THIRDPARTY # vcs
+
+ assert len(finder.mapping) > 100
+ assert finder._normalize_name('deal') == 'deal'
+ assert finder._normalize_name('Django') == 'django' # lowercase
+ assert finder._normalize_name('django_haystack') == 'haystack' # mapping
+ assert finder._normalize_name('Flask-RESTful') == 'flask_restful' # conver `-`to `_`
+
+ req_file.remove()
+
+
+PIPFILE = """
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[requires]
+python_version = "3.5"
+
+[packages]
+Django = "~=1.11"
+deal = {editable = true, git = "https://github.com/orsinium/deal.git"}
+
+[dev-packages]
+"""
+
+
+def test_pipfile_finder(tmpdir):
+ pipfile = tmpdir.join('Pipfile')
+ pipfile.write(PIPFILE)
+ si = SortImports(file_contents="")
+ finder = finders.PipfileFinder(
+ config=si.config,
+ sections=si.sections,
+ path=str(tmpdir)
+ )
+
+ assert set(finder._get_names(str(tmpdir))) == {'Django', 'deal'} # file parsing
+
+ assert finder.find("django") == si.sections.THIRDPARTY # package in reqs
+ assert finder.find("flask") is None # package not in reqs
+ assert finder.find("deal") == si.sections.THIRDPARTY # vcs
+
+ assert len(finder.mapping) > 100
+ assert finder._normalize_name('deal') == 'deal'
+ assert finder._normalize_name('Django') == 'django' # lowercase
+ assert finder._normalize_name('django_haystack') == 'haystack' # mapping
+ assert finder._normalize_name('Flask-RESTful') == 'flask_restful' # conver `-`to `_`
+
+ pipfile.remove()
diff --git a/tox.ini b/tox.ini
index dbc0ea86..7b0e0d31 100644
--- a/tox.ini
+++ b/tox.ini
@@ -5,7 +5,11 @@ envlist =
py{27,34,35,36,37,py}
[testenv]
-deps = pytest
+deps =
+ pytest
+ pip
+ pipreqs
+ requirementslib
commands = py.test test_isort.py {posargs}
[testenv:isort-check]