summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTimothy Edmund Crosley <timothy.crosley@gmail.com>2019-03-02 17:50:26 -0800
committerGitHub <noreply@github.com>2019-03-02 17:50:26 -0800
commit36dd2e03a5e7ebe89ad5a928613c8e7062609d78 (patch)
treebfd7fc12e3ccba44aa2fa586020a0f8224a98350
parente5572e95aaf08e4ee88e4ddcec38b0906d87741b (diff)
parent0b36a8bcf29f092c427c5dc8f5eb6e79cfa4134b (diff)
downloadisort-36dd2e03a5e7ebe89ad5a928613c8e7062609d78.tar.gz
Merge branch 'develop' into cov
-rw-r--r--.editorconfig20
-rw-r--r--CHANGELOG.md3
-rw-r--r--README.rst2
-rw-r--r--isort/__init__.py2
-rw-r--r--isort/finders.py31
-rw-r--r--isort/isort.py33
-rw-r--r--isort/main.py2
-rw-r--r--isort/settings.py24
-rwxr-xr-xsetup.py2
-rw-r--r--test_isort.py152
-rw-r--r--tox.ini2
11 files changed, 200 insertions, 73 deletions
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..094bc546
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,20 @@
+root = true
+
+[*.py]
+max_line_length = 120
+indent_style = space
+indent_size = 4
+known_first_party = isort
+known_third_party = kate
+ignore_frosted_errors = E103
+skip = build,.tox,venv
+balanced_wrapping = true
+not_skip = __init__.py
+
+[*.{rst,ini}]
+indent_style = space
+indent_size = 4
+
+[*.yml]
+indent_style = space
+indent_size = 2
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e72551a6..6635a5af 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,9 @@ Internal:
Planned:
- profile support for common project types (black, django, google, etc)
+### 4.3.9 - Feburary 25, 2019 - hot fix release
+- Fixed a bug that led to an incompatibility with black: #831
+
### 4.3.8 - Feburary 25, 2019 - hot fix release
- Fixed a bug that led to the recursive option not always been available from the command line.
diff --git a/README.rst b/README.rst
index e7419384..90e4f9a5 100644
--- a/README.rst
+++ b/README.rst
@@ -328,7 +328,7 @@ past the line_length limit and has 6 possible settings:
In Mode 5 isort leaves a single extra space to maintain consistency of output when a comma is added at the end.
Mode 6 is the same - except that no extra space is maintained leading to the possibility of lines one character longer.
-You can enforce a trailing comma by using this in conjunction with `-tc` or `trailing_comma: True`.
+You can enforce a trailing comma by using this in conjunction with ``-tc`` or ``include_trailing_comma: True``.
.. code-block:: python
diff --git a/isort/__init__.py b/isort/__init__.py
index 3553b811..63034de6 100644
--- a/isort/__init__.py
+++ b/isort/__init__.py
@@ -22,4 +22,4 @@ OTHER DEALINGS IN THE SOFTWARE.
from . import settings # noqa: F401
from .isort import SortImports # noqa: F401
-__version__ = "4.3.8"
+__version__ = "4.3.9"
diff --git a/isort/finders.py b/isort/finders.py
index e21ef447..ccf6caac 100644
--- a/isort/finders.py
+++ b/isort/finders.py
@@ -8,6 +8,7 @@ import sys
import sysconfig
from abc import ABCMeta, abstractmethod
from fnmatch import fnmatch
+from functools import lru_cache
from glob import glob
from typing import Any, Dict, Iterable, Iterator, List, Mapping, Optional, Pattern, Sequence, Tuple, Type
@@ -34,7 +35,6 @@ try:
except ImportError:
Pipfile = None
-
KNOWN_SECTION_MAPPING = {
'STDLIB': 'STANDARD_LIBRARY',
'FUTURE': 'FUTURE_LIBRARY',
@@ -268,6 +268,13 @@ class RequirementsFinder(ReqsBaseFinder):
def _get_files_from_dir(self, path: str) -> Iterator[str]:
"""Return paths to requirements files from passed dir.
"""
+ return RequirementsFinder._get_files_from_dir_cached(path)
+
+ @classmethod
+ @lru_cache(maxsize=16)
+ def _get_files_from_dir_cached(cls, path):
+ results = []
+
for fname in os.listdir(path):
if 'requirements' not in fname:
continue
@@ -276,26 +283,38 @@ class RequirementsFinder(ReqsBaseFinder):
# *requirements*/*.{txt,in}
if os.path.isdir(full_path):
for subfile_name in os.listdir(path):
- for ext in self.exts:
+ for ext in cls.exts:
if subfile_name.endswith(ext):
- yield os.path.join(path, subfile_name)
+ results.append(os.path.join(path, subfile_name))
continue
# *requirements*.{txt,in}
if os.path.isfile(full_path):
- for ext in self.exts:
+ for ext in cls.exts:
if fname.endswith(ext):
- yield full_path
+ results.append(full_path)
break
+ return results
+
def _get_names(self, path: str) -> Iterator[str]:
"""Load required packages from path to requirements file
"""
+ for i in RequirementsFinder._get_names_cached(path):
+ yield i
+
+ @classmethod
+ @lru_cache(maxsize=16)
+ def _get_names_cached(cls, path: str) -> List[str]:
+ result = []
+
with chdir(os.path.dirname(path)):
requirements = parse_requirements(path, session=PipSession())
for req in requirements:
if req.name:
- yield req.name
+ result.append(req.name)
+
+ return result
class PipfileFinder(ReqsBaseFinder):
diff --git a/isort/isort.py b/isort/isort.py
index ca28d56d..44a1c9e3 100644
--- a/isort/isort.py
+++ b/isort/isort.py
@@ -283,13 +283,10 @@ class SortImports(object):
ignore_case: bool = False,
section_name: Optional[Any] = None
) -> str:
- dots = 0
- while module_name.startswith('.'):
- dots += 1
- module_name = module_name[1:]
-
- if dots:
- module_name = '{} {}'.format(('.' * dots), module_name)
+ match = re.match(r'^(\.+)\s*(.*)', module_name)
+ if match:
+ sep = ' ' if config['reverse_relative'] else '_'
+ module_name = sep.join(match.groups())
prefix = ""
if ignore_case:
@@ -558,7 +555,7 @@ class SortImports(object):
sections = ('no_sections', )
output = [] # type: List[str]
- prev_section_has_imports = False
+ pending_lines_before = False
for section in sections:
straight_modules = self.imports[section]['straight']
straight_modules = nsorted(straight_modules, key=lambda key: self._module_key(key, self.config, section_name=section))
@@ -591,8 +588,11 @@ class SortImports(object):
line = line.lower()
return '{0}{1}'.format(section, line)
section_output = nsorted(section_output, key=by_module)
+
+ section_name = section
+ no_lines_before = section_name in self.config['no_lines_before']
+
if section_output:
- section_name = section
if section_name in self.place_imports:
self.place_imports[section_name] = section_output
continue
@@ -602,11 +602,16 @@ class SortImports(object):
section_comment = "# {0}".format(section_title)
if section_comment not in self.out_lines[0:1] and section_comment not in self.in_lines[0:1]:
section_output.insert(0, section_comment)
- if prev_section_has_imports and section_name in self.config['no_lines_before']:
- while output and output[-1].strip() == '':
- output.pop()
- output += section_output + ([''] * self.config['lines_between_sections'])
- prev_section_has_imports = bool(section_output)
+
+ if pending_lines_before or not no_lines_before:
+ output += ([''] * self.config['lines_between_sections'])
+
+ output += section_output
+
+ pending_lines_before = False
+ else:
+ pending_lines_before = pending_lines_before or not no_lines_before
+
while output and output[-1].strip() == '':
output.pop()
while output and output[0].strip() == '':
diff --git a/isort/main.py b/isort/main.py
index a2d4b935..80dbc712 100644
--- a/isort/main.py
+++ b/isort/main.py
@@ -253,6 +253,8 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]:
parser.add_argument('-r', dest='ambiguous_r_flag', action='store_true')
parser.add_argument('-rm', '--remove-import', dest='remove_imports', action='append',
help='Removes the specified import from all files.')
+ parser.add_argument('-rr', '--reverse-relative', dest='reverse_relative', action='store_true',
+ help='Reverse order of relative imports.')
parser.add_argument('-rc', '--recursive', dest='recursive', action='store_true',
help='Recursively look for Python files of which to sort imports')
parser.add_argument('-s', '--skip', help='Files that sort imports should skip over. If you want to skip multiple '
diff --git a/isort/settings.py b/isort/settings.py
index c1d17802..3dbadd15 100644
--- a/isort/settings.py
+++ b/isort/settings.py
@@ -28,11 +28,10 @@ import fnmatch
import os
import posixpath
import re
-import stat
import warnings
from distutils.util import strtobool
from functools import lru_cache
-from typing import Any, Dict, Iterable, List, Mapping, MutableMapping, Type
+from typing import Any, Dict, Iterable, List, Mapping, MutableMapping, Callable
from .utils import difference, union
@@ -68,7 +67,7 @@ class WrapModes(enum.Enum):
@staticmethod
def from_string(value: str) -> 'WrapModes':
- return WrapModes(int(value))
+ return getattr(WrapModes, value, None) or WrapModes(int(value))
# Note that none of these lists must be complete as they are simply fallbacks for when included auto-detection fails.
@@ -135,6 +134,7 @@ default = {'force_to_top': [],
'length_sort': False,
'add_imports': [],
'remove_imports': [],
+ 'reverse_relative': False,
'force_single_line': False,
'default_section': 'FIRSTPARTY',
'import_heading_future': '',
@@ -216,6 +216,13 @@ def _update_settings_with_config(
_update_with_config_file(editor_config_file, sections, computed_settings)
+def _get_str_to_type_converter(setting_name: str) -> Callable[[str], Any]:
+ type_converter = type(default.get(setting_name, '')) # type: Callable[[str], Any]
+ if type_converter == WrapModes:
+ type_converter = WrapModes.from_string
+ return type_converter
+
+
def _update_with_config_file(
file_path: str,
sections: Iterable[str],
@@ -243,7 +250,7 @@ def _update_with_config_file(
for key, value in settings.items():
access_key = key.replace('not_', '').lower()
- existing_value_type = type(default.get(access_key, '')) # type: Type[Any]
+ existing_value_type = _get_str_to_type_converter(access_key)
if existing_value_type in (list, tuple):
# sections has fixed order values; no adding or substraction from any set
if access_key == 'sections':
@@ -271,7 +278,7 @@ def _update_with_config_file(
result = default.get(access_key) if value.lower().strip() == 'false' else 2
computed_settings[access_key] = result
else:
- computed_settings[access_key] = existing_value_type(value)
+ computed_settings[access_key] = getattr(existing_value_type, str(value), None) or existing_value_type(value)
def _as_list(value: str) -> List[str]:
@@ -337,7 +344,10 @@ def should_skip(
path: str = '/'
) -> bool:
"""Returns True if the file should be skipped based on the passed in settings."""
- normalized_path = posixpath.join(path.replace('\\', '/'), filename)
+ os_path = os.path.join(path, filename)
+ normalized_path = os_path.replace('\\', '/')
+ if normalized_path[1:2] == ':':
+ normalized_path = normalized_path[2:]
if config['safety_excludes'] and safety_exclude_re.search(normalized_path):
return True
@@ -356,7 +366,7 @@ def should_skip(
if fnmatch.fnmatch(filename, glob):
return True
- if stat.S_ISFIFO(os.stat(normalized_path).st_mode):
+ if not (os.path.isfile(os_path) or os.path.isdir(os_path) or os.path.islink(os_path)):
return True
return False
diff --git a/setup.py b/setup.py
index fbcb933e..6858ad4e 100755
--- a/setup.py
+++ b/setup.py
@@ -6,7 +6,7 @@ with open('README.rst') as f:
readme = f.read()
setup(name='isort',
- version='4.3.8',
+ version='4.3.9',
description='A Python utility / library to sort Python imports.',
long_description=readme,
author='Timothy Crosley',
diff --git a/test_isort.py b/test_isort.py
index b35d6cdd..f1f6bbe0 100644
--- a/test_isort.py
+++ b/test_isort.py
@@ -21,11 +21,14 @@ OTHER DEALINGS IN THE SOFTWARE.
"""
from tempfile import NamedTemporaryFile
+import io
import os
import os.path
+import posixpath
import sys
import sysconfig
+import py
import pytest
from isort import finders, main, settings
@@ -51,7 +54,6 @@ skip = build,.tox,venv
balanced_wrapping = true
not_skip = __init__.py
"""
-
SHORT_IMPORT = "from third_party import lib1, lib2, lib3, lib4"
SINGLE_FROM_IMPORT = "from third_party import lib1"
SINGLE_LINE_LONG_IMPORT = "from third_party import lib1, lib2, lib3, lib4, lib5, lib5ab"
@@ -68,8 +70,9 @@ def default_settings_path(tmpdir_factory):
config_file = config_dir.join('.editorconfig').strpath
with open(config_file, 'w') as editorconfig:
editorconfig.write(TEST_DEFAULT_CONFIG)
- os.chdir(config_dir.strpath)
- return config_dir.strpath
+
+ with config_dir.as_cwd():
+ yield config_dir.strpath
def test_happy_path():
@@ -565,7 +568,7 @@ def test_skip_with_file_name():
test_input = ("import django\n"
"import myproject\n")
- sort_imports = SortImports(file_path='/baz.py', file_contents=test_input, known_third_party=['django'],
+ sort_imports = SortImports(file_path='/baz.py', file_contents=test_input, settings_path=os.getcwd(),
skip=['baz.py'])
assert sort_imports.skipped
assert sort_imports.output is None
@@ -1721,7 +1724,7 @@ def test_other_file_encodings(tmpdir):
tmp_fname = tmpdir.join('test_{0}.py'.format(encoding))
file_contents = "# coding: {0}\n\ns = u'ã'\n".format(encoding)
tmp_fname.write_binary(file_contents.encode(encoding))
- assert SortImports(file_path=str(tmp_fname)).output == file_contents
+ assert SortImports(file_path=str(tmp_fname), settings_path=os.getcwd()).output == file_contents
def test_comment_at_top_of_file():
@@ -2243,7 +2246,8 @@ def test_inconsistent_behavior_in_python_2_and_3_issue_479():
"""Test to ensure Python 2 and 3 have the same behavior"""
test_input = ('from future.standard_library import hooks\n'
'from workalendar.europe import UnitedKingdom\n')
- assert SortImports(file_contents=test_input).output == test_input
+ assert SortImports(file_contents=test_input,
+ known_first_party=["future"]).output == test_input
def test_sort_within_section_comments_issue_436():
@@ -2394,6 +2398,18 @@ def test_not_splitted_sections():
assert SortImports(file_contents=test_input, no_lines_before=['STDLIB']).output == test_input
+def test_no_lines_before_empty_section():
+ test_input = ('import first\n'
+ 'import custom\n')
+ assert SortImports(
+ file_contents=test_input,
+ known_third_party=["first"],
+ known_custom=["custom"],
+ sections=['THIRDPARTY', 'LOCALFOLDER', 'CUSTOM'],
+ no_lines_before=['THIRDPARTY', 'LOCALFOLDER', 'CUSTOM'],
+ ).output == test_input
+
+
def test_no_inline_sort():
"""Test to ensure multiple `from` imports in one line are not sorted if `--no-inline-sort` flag
is enabled. If `--force-single-line-imports` flag is enabled, then `--no-inline-sort` is ignored."""
@@ -2516,32 +2532,49 @@ def test_to_ensure_tabs_dont_become_space_issue_665():
def test_new_lines_are_preserved():
- with NamedTemporaryFile('w', suffix='py') as rn_newline:
- with open(rn_newline.name, 'w', newline='') as rn_newline_input:
+ with NamedTemporaryFile('w', suffix='py', delete=False) as rn_newline:
+ pass
+
+ try:
+ with io.open(rn_newline.name, mode='w', newline='') as rn_newline_input:
rn_newline_input.write('import sys\r\nimport os\r\n')
- rn_newline_input.flush()
- SortImports(rn_newline.name)
- with open(rn_newline.name, newline='') as rn_newline_file:
- rn_newline_contents = rn_newline_file.read()
- assert rn_newline_contents == 'import os\r\nimport sys\r\n'
-
- with NamedTemporaryFile('w', suffix='py') as r_newline:
- with open(r_newline.name, 'w', newline='') as r_newline_input:
+
+ SortImports(rn_newline.name, settings_path=os.getcwd())
+ with io.open(rn_newline.name) as new_line_file:
+ print(new_line_file.read())
+ with io.open(rn_newline.name, newline='') as rn_newline_file:
+ rn_newline_contents = rn_newline_file.read()
+ assert rn_newline_contents == 'import os\r\nimport sys\r\n'
+ finally:
+ os.remove(rn_newline.name)
+
+ with NamedTemporaryFile('w', suffix='py', delete=False) as r_newline:
+ pass
+
+ try:
+ with io.open(r_newline.name, mode='w', newline='') as r_newline_input:
r_newline_input.write('import sys\rimport os\r')
- r_newline_input.flush()
- SortImports(r_newline.name)
- with open(r_newline.name, newline='') as r_newline_file:
- r_newline_contents = r_newline_file.read()
- assert r_newline_contents == 'import os\rimport sys\r'
-
- with NamedTemporaryFile('w', suffix='py') as n_newline:
- with open(n_newline.name, 'w', newline='') as n_newline_input:
+
+ SortImports(r_newline.name, settings_path=os.getcwd())
+ with io.open(r_newline.name, newline='') as r_newline_file:
+ r_newline_contents = r_newline_file.read()
+ assert r_newline_contents == 'import os\rimport sys\r'
+ finally:
+ os.remove(r_newline.name)
+
+ with NamedTemporaryFile('w', suffix='py', delete=False) as n_newline:
+ pass
+
+ try:
+ with io.open(n_newline.name, mode='w', newline='') as n_newline_input:
n_newline_input.write('import sys\nimport os\n')
- n_newline_input.flush()
- SortImports(n_newline.name)
- with 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'
+
+ SortImports(n_newline.name, settings_path=os.getcwd())
+ 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'
+ finally:
+ os.remove(n_newline.name)
@pytest.mark.skipif(not finders.RequirementsFinder.enabled, reason='RequirementsFinder not enabled (too old version of pip?)')
@@ -2665,11 +2698,11 @@ def test_path_finder(monkeypatch):
third_party_prefix = next(path for path in finder.paths if "site-packages" in path)
ext_suffix = sysconfig.get_config_var("EXT_SUFFIX") or ".so"
imaginary_paths = set([
- os.path.join(finder.stdlib_lib_prefix, "example_1.py"),
- os.path.join(third_party_prefix, "example_2.py"),
- os.path.join(third_party_prefix, "example_3.so"),
- os.path.join(third_party_prefix, "example_4" + ext_suffix),
- os.path.join(os.getcwd(), "example_5.py"),
+ posixpath.join(finder.stdlib_lib_prefix, "example_1.py"),
+ posixpath.join(third_party_prefix, "example_2.py"),
+ posixpath.join(third_party_prefix, "example_3.so"),
+ posixpath.join(third_party_prefix, "example_4" + ext_suffix),
+ posixpath.join(os.getcwd(), "example_5.py"),
])
monkeypatch.setattr("isort.finders.exists_case_sensitive", lambda p: p in imaginary_paths)
assert finder.find("example_1") == finder.sections.STDLIB
@@ -2693,17 +2726,18 @@ def test_command_line(tmpdir, capfd, multiprocess):
from isort.main import main
tmpdir.join("file1.py").write("import re\nimport os\n\nimport contextlib\n\n\nimport isort")
tmpdir.join("file2.py").write("import collections\nimport time\n\nimport abc\n\n\nimport isort")
- arguments = ["-rc", str(tmpdir)]
+ arguments = ["-rc", str(tmpdir), '--settings-path', os.getcwd()]
if multiprocess:
arguments.extend(['--jobs', '2'])
main(arguments)
assert tmpdir.join("file1.py").read() == "import contextlib\nimport os\nimport re\n\nimport isort\n"
assert tmpdir.join("file2.py").read() == "import abc\nimport collections\nimport time\n\nimport isort\n"
- out, err = capfd.readouterr()
- assert not err
- # it informs us about fixing the files:
- assert str(tmpdir.join("file1.py")) in out
- assert str(tmpdir.join("file2.py")) in out
+ if not sys.platform.startswith('win'):
+ out, err = capfd.readouterr()
+ assert not err
+ # it informs us about fixing the files:
+ assert str(tmpdir.join("file1.py")) in out
+ assert str(tmpdir.join("file2.py")) in out
@pytest.mark.parametrize('enabled', (False, True))
@@ -2713,12 +2747,15 @@ def test_safety_excludes(tmpdir, enabled):
tmpdir.mkdir("lib").mkdir("python3.7").join("importantsystemlibrary.py").write("# ...")
config = dict(settings.default.copy(), safety_excludes=enabled)
skipped = []
+ codes = [str(tmpdir)],
+ main.iter_source_code(codes, config, skipped)
file_names = set(os.path.relpath(f, str(tmpdir)) for f in main.iter_source_code([str(tmpdir)], config, skipped))
if enabled:
assert file_names == {'victim.py'}
assert len(skipped) == 2
else:
- assert file_names == {'.tox/verysafe.py', 'lib/python3.7/importantsystemlibrary.py', 'victim.py'}
+ assert file_names == {os.sep.join(('.tox', 'verysafe.py')),
+ os.sep.join(('lib', 'python3.7', 'importantsystemlibrary.py')), 'victim.py'}
assert not skipped
@@ -2729,7 +2766,7 @@ def test_comments_not_removed_issue_576():
assert SortImports(file_contents=test_input).output == test_input
-def test_inconsistent_relative_imports_issue_577():
+def test_reverse_relative_imports_issue_417():
test_input = ('from . import ipsum\n'
'from . import lorem\n'
'from .dolor import consecteur\n'
@@ -2742,6 +2779,24 @@ def test_inconsistent_relative_imports_issue_577():
'from ... import dui\n'
'from ...eu import dignissim\n'
'from ...ex import metus\n')
+ assert SortImports(file_contents=test_input,
+ force_single_line=True,
+ reverse_relative=True).output == test_input
+
+
+def test_inconsistent_relative_imports_issue_577():
+ test_input = ('from ... import diam\n'
+ 'from ... import dui\n'
+ 'from ...eu import dignissim\n'
+ 'from ...ex import metus\n'
+ 'from .. import donec\n'
+ 'from .. import euismod\n'
+ 'from ..mi import iaculis\n'
+ 'from ..nec import tempor\n'
+ 'from . import ipsum\n'
+ 'from . import lorem\n'
+ 'from .dolor import consecteur\n'
+ 'from .sit import apidiscing\n')
assert SortImports(file_contents=test_input, force_single_line=True).output == test_input
@@ -2772,3 +2827,16 @@ def test_noqa_issue_679():
'import zed # NOQA\n'
'import ujson # NOQA\n')
assert SortImports(file_contents=test_input).output == test_output
+
+
+def test_extract_multiline_output_wrap_setting_from_a_config_file(tmpdir: py.path.local) -> None:
+ editorconfig_contents = [
+ 'root = true',
+ ' [*.py]',
+ 'multi_line_output = 5'
+ ]
+ config_file = tmpdir.join('.editorconfig')
+ config_file.write('\n'.join(editorconfig_contents))
+
+ config = settings.from_path(str(tmpdir))
+ assert config['multi_line_output'] == WrapModes.VERTICAL_GRID_GROUPED
diff --git a/tox.ini b/tox.ini
index 0b157187..95edf584 100644
--- a/tox.ini
+++ b/tox.ini
@@ -16,7 +16,7 @@ extras =
requirements
setenv =
coverage: PYTEST_ADDOPTS=--cov {env:PYTEST_ADDOPTS:}
-commands = pytest {posargs}
+commands = py.test -vv -s test_isort.py {posargs}
[testenv:isort-check]
basepython = python3