Repository: timothycrosley/isort
Pull request #840 (2019-02-25 21:46:54)

Keep trailing comma even for a single from import. Issue 831
Commit https://github.com/timothycrosley/isort/commit/75b354995028c66a801c8c0e60f817b60fc64e8e removed commas if there was only one import, ostensibly to conform to Black, but currently Black behavior is such
```
MBP-jpaige% cat test.py
from some.very.lengthy.example.library.that.goes.too.far.subdomain1 import (
    AnObnoxiouslyLongClassName1,
)

from some.very.lengthy.example.library.that.goes.too.far.subdomain2 import (
    AnObnoxiouslyLongClassName2,)

from some.very.lengthy.example.library.that.goes.too.far.subdomain3 import (AnObnoxiouslyLongClassName3,)

from some.very.lengthy.example.library.that.goes.too.far.subdomain4 import (AnObnoxiouslyLongClassName4,
)

from some.very.lengthy.example.library.that.goes.too.far.subdomain5 import AnObnoxiouslyLongClassName5

from some.very.lengthy.example.library.that.goes.too.far.subdomain6 import (
    AnObnoxiouslyLongClassName6,
    AnObnoxiouslyLongClassName7,
)

from some.library.subdomain7 import (
    AnObnoxiouslyLongClassName8,
    AnObnoxiouslyLongClassName9,
)
MBP-jpaige% python3 -m pip show black
Name: black
Version: 18.9b0
Summary: The uncompromising code formatter.
Home-page: https://github.com/ambv/black
Author: Łukasz Langa
Author-email: lukasz@langa.pl
License: MIT
Location: /usr/local/lib/python3.7/site-packages
Requires: toml, click, attrs, appdirs
Required-by:
MBP-jpaige% python3 -m black test.py
reformatted test.py
All done! ✨ 🍰 ✨
1 file reformatted.
MBP-jpaige% cat test.py
from some.very.lengthy.example.library.that.goes.too.far.subdomain1 import (
    AnObnoxiouslyLongClassName1,
)

from some.very.lengthy.example.library.that.goes.too.far.subdomain2 import (
    AnObnoxiouslyLongClassName2,
)

from some.very.lengthy.example.library.that.goes.too.far.subdomain3 import (
    AnObnoxiouslyLongClassName3,
)

from some.very.lengthy.example.library.that.goes.too.far.subdomain4 import (
    AnObnoxiouslyLongClassName4,
)

from some.very.lengthy.example.library.that.goes.too.far.subdomain5 import (
    AnObnoxiouslyLongClassName5,
)

from some.very.lengthy.example.library.that.goes.too.far.subdomain6 import (
    AnObnoxiouslyLongClassName6,
    AnObnoxiouslyLongClassName7,
)

from some.library.subdomain7 import (
    AnObnoxiouslyLongClassName8,
    AnObnoxiouslyLongClassName9,
)
```

And this has been noted in https://github.com/timothycrosley/isort/issues/831


Patch:
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..8e6e1941 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,26 +1,11 @@
 Changelog
 =========
 
-### 5.0.0 UNRELEASED
-**Breaking changes:**
-  - isort now requires Python 3.4+ to run but continues to support formatting
+### 4.4.0 UNRELEASED
+- **Breaking changes:**
+  - isort now requires Python 3.4+ to run but continues to support formating
     Python 2 code.
 
-Internal:
-  - isort now utilizes mypy and typing to filter out typing related issues before deployment
-
-Planned:
-  - profile support for common project types (black, django, google, etc)
-
-### 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.
-
-### 4.3.7 - February 25, 2019 - hot fix release
-- Expands the finder failsafe to occur on the creation of the finder objects.
-
-### 4.3.6 - February 24, 2019 - hot fix release
-- Fixes a fatal error that occurs if a single finder throws an exception. Important as we add more finders that utilize third party libraries.
-
 ### 4.3.5 - February 24, 2019 - last Python 2.7 Maintenance Release
 
 This is the final Python 2.x release of isort, and includes the following major changes:
diff --git a/LICENSE b/LICENSE
old mode 100644
new mode 100755
diff --git a/isort/__init__.py b/isort/__init__.py
index 3553b811..d59d0785 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.5"
diff --git a/isort/finders.py b/isort/finders.py
index e21ef447..866fb680 100644
--- a/isort/finders.py
+++ b/isort/finders.py
@@ -332,32 +332,14 @@ class FindersManager(object):
         self,
         config: Mapping[str, Any],
         sections: Any,
-        finder_classes: Optional[Iterable[Type[BaseFinder]]] = None
+        finders: Optional[Iterable[BaseFinder]] = None
     ) -> None:
-        self.verbose = config.get('verbose', False)  # type: bool
-
-        if finder_classes is None:
-            finder_classes = self._default_finders_classes
-        finders = []  # type: List[BaseFinder]
-        for finder_cls in finder_classes:
-            try:
-                finders.append(finder_cls(config, sections))
-            except Exception as exception:
-                # if one finder fails to instantiate isort can continue using the rest
-                if self.verbose:
-                    print('{} encountered an error ({}) during instantiation and cannot be used'.format(finder_cls.__name__,
-                                                                                                        str(exception)))
-        self.finders = tuple(finders)  # type: Tuple[BaseFinder, ...]
+        if finders is not None:
+            self.finders = finders
+        self.finders = tuple(finder_cls(config, sections) for finder_cls in self._default_finders_classes)
 
     def find(self, module_name: str) -> Optional[str]:
         for finder in self.finders:
-            try:
-                section = finder.find(module_name)
-            except Exception as exception:
-                # isort has to be able to keep trying to identify the correct import section even if one approach fails
-                if self.verbose:
-                    print('{} encountered an error ({}) while trying to identify the {} module'.format(finder.__class__.__name__,
-                                                                                                       str(exception),
-                                                                                                       module_name))
+            section = finder.find(module_name)
             if section is not None:
                 return section
diff --git a/isort/isort.py b/isort/isort.py
index 2bd6b9f3..56e7bf89 100644
--- a/isort/isort.py
+++ b/isort/isort.py
@@ -37,7 +37,6 @@ from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Mapping, Optional,
 from . import settings
 from .finders import FindersManager
 from .natural import nsorted
-from .settings import WrapModes
 
 if TYPE_CHECKING:
     from mypy_extensions import TypedDict
@@ -335,7 +334,7 @@ class SortImports(object):
             Returns an import wrapped to the specified line-length, if possible.
         """
         wrap_mode = self.config['multi_line_output']
-        if len(line) > self.config['line_length'] and wrap_mode != WrapModes.NOQA:
+        if len(line) > self.config['line_length'] and wrap_mode != settings.WrapModes.NOQA:
             line_without_comment = line
             comment = None
             if '#' in line:
@@ -358,9 +357,10 @@ class SortImports(object):
                         output = "{0}{1}({2}{3}{4}{5})".format(
                             line, splitter, self.line_separator, cont_line,
                             "," if self.config['include_trailing_comma'] else "",
-                            self.line_separator if wrap_mode in {WrapModes.VERTICAL_HANGING_INDENT,
-                                                                 WrapModes.VERTICAL_GRID_GROUPED}
-                            else "")
+                            self.line_separator if wrap_mode in (
+                                settings.WrapModes.VERTICAL_HANGING_INDENT,
+                                settings.WrapModes.VERTICAL_GRID_GROUPED,
+                            ) else "")
                         lines = output.split(self.line_separator)
                         if self.config['comment_prefix'] in lines[-1] and lines[-1].endswith(')'):
                             line, comment = lines[-1].split(self.config['comment_prefix'], 1)
@@ -491,20 +491,20 @@ class SortImports(object):
 
                     # If line too long AND have imports AND we are NOT using GRID or VERTICAL wrap modes
                     if (len(import_statement) > self.config['line_length'] and len(from_import_section) > 0 and
-                            self.config['multi_line_output'] not in (settings.WrapModes.GRID, settings.WrapModes.VERTICAL)):
+                            self.config['multi_line_output'] not in (1, 0)):
                         do_multiline_reformat = True
 
                     if do_multiline_reformat:
                         import_statement = self._multi_line_reformat(import_start, from_import_section, comments)
-                        if self.config['multi_line_output'] == settings.WrapModes.GRID:
-                            self.config['multi_line_output'] = settings.WrapModes.VERTICAL_GRID
+                        if self.config['multi_line_output'] == 0:
+                            self.config['multi_line_output'] = 4
                             try:
                                 other_import_statement = self._multi_line_reformat(import_start, from_import_section, comments)
                                 if (max(len(x)
                                         for x in import_statement.split('\n')) > self.config['line_length']):
                                     import_statement = other_import_statement
                             finally:
-                                self.config['multi_line_output'] = settings.WrapModes.GRID
+                                self.config['multi_line_output'] = 0
                     if not do_multiline_reformat and len(import_statement) > self.config['line_length']:
                         import_statement = self._wrap(import_statement)
 
@@ -515,7 +515,7 @@ class SortImports(object):
                     section_output.append(import_statement)
 
     def _multi_line_reformat(self, import_start: str, from_imports: List[str], comments: Sequence[str]) -> str:
-        output_mode = self.config['multi_line_output'].name.lower()
+        output_mode = settings.WrapModes._fields[self.config['multi_line_output']].lower()
         formatter = getattr(self, "_output_" + output_mode, self._output_grid)
         dynamic_indent = " " * (len(import_start) + 1)
         indent = self.config['indent']
@@ -734,17 +734,13 @@ class SortImports(object):
         line_length: int,
         comments: Sequence[str]
     ) -> str:
-        trailing = ""
-        if len(imports) > 1 and self.config['include_trailing_comma']:
-            trailing = ','
-
         return "{0}({1}{2}{3}{4}{5}{2})".format(
             statement,
             self._add_comments(comments),
             self.line_separator,
             indent,
             ("," + self.line_separator + indent).join(imports),
-            trailing,
+            "," if self.config['include_trailing_comma'] else "",
          )
 
     def _output_vertical_grid_common(
diff --git a/isort/main.py b/isort/main.py
index a2d4b935..0cad6904 100644
--- a/isort/main.py
+++ b/isort/main.py
@@ -233,7 +233,7 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]:
                         help="Forces line endings to the specified value. If not set, values will be guessed per-file.")
     parser.add_argument('-ls', '--length-sort', help='Sort imports by their string length.',
                         dest='length_sort', action='store_true')
-    parser.add_argument('-m', '--multi-line', dest='multi_line_output', type=WrapModes.from_string,
+    parser.add_argument('-m', '--multi-line', dest='multi_line_output', type=int, choices=range(len(WrapModes)),
                         help='Multi line output (0-grid, 1-vertical, 2-hanging, 3-vert-hanging, 4-vert-grid, '
                         '5-vert-grid-grouped, 6-vert-grid-grouped-no-comma).')
     inline_args_group.add_argument('-nis', '--no-inline-sort', dest='no_inline_sort', action='store_true',
@@ -250,7 +250,6 @@ def parse_args(argv: Optional[Sequence[str]] = None) -> Dict[str, Any]:
                         help='Force sortImports to recognize a module as being part of the current python project.')
     parser.add_argument('-q', '--quiet', action='store_true', dest="quiet",
                         help='Shows extra quiet output, only errors are outputted.')
-    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('-rc', '--recursive', dest='recursive', action='store_true',
@@ -306,11 +305,6 @@ def main(argv: Optional[Sequence[str]] = None) -> None:
         print(INTRO)
         return
 
-    if arguments.get('ambiguous_r_flag'):
-        print('ERROR: Deprecated -r flag set. This flag has been replaced with -rm to remove ambiguity between it and '
-              '-rc for recursive')
-        sys.exit(1)
-
     if 'settings_path' in arguments:
         sp = arguments['settings_path']
         arguments['settings_path'] = os.path.abspath(sp) if os.path.isdir(sp) else os.path.dirname(os.path.abspath(sp))
diff --git a/isort/settings.py b/isort/settings.py
index c1d17802..887a2572 100644
--- a/isort/settings.py
+++ b/isort/settings.py
@@ -23,13 +23,13 @@ OTHER DEALINGS IN THE SOFTWARE.
 
 """
 import configparser
-import enum
 import fnmatch
 import os
 import posixpath
 import re
 import stat
 import warnings
+from collections import namedtuple
 from distutils.util import strtobool
 from functools import lru_cache
 from typing import Any, Dict, Iterable, List, Mapping, MutableMapping, Type
@@ -55,21 +55,9 @@ safety_exclude_re = re.compile(
     r"/(\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.tox|\.venv|_build|buck-out|build|dist|lib/python[0-9].[0-9]+)/"
 )
 
-
-class WrapModes(enum.Enum):
-    GRID = 0  # 0
-    VERTICAL = 1
-    HANGING_INDENT = 2
-    VERTICAL_HANGING_INDENT = 3
-    VERTICAL_GRID = 4
-    VERTICAL_GRID_GROUPED = 5
-    VERTICAL_GRID_GROUPED_NO_COMMA = 6
-    NOQA = 7
-
-    @staticmethod
-    def from_string(value: str) -> 'WrapModes':
-        return WrapModes(int(value))
-
+_wrap_mode_strings = ('GRID', 'VERTICAL', 'HANGING_INDENT', 'VERTICAL_HANGING_INDENT', 'VERTICAL_GRID', 'VERTICAL_GRID_GROUPED',
+                      'VERTICAL_GRID_GROUPED_NO_COMMA', 'NOQA')
+WrapModes = namedtuple('WrapModes', _wrap_mode_strings)(*range(len(_wrap_mode_strings)))  # type: Any
 
 # Note that none of these lists must be complete as they are simply fallbacks for when included auto-detection fails.
 default = {'force_to_top': [],
@@ -128,7 +116,7 @@ default = {'force_to_top': [],
                                       'zipimport', 'zlib'],
            'known_third_party': ['google.appengine.api'],
            'known_first_party': [],
-           'multi_line_output': WrapModes.GRID,
+           'multi_line_output': WrapModes.GRID,  # type: ignore
            'forced_separate': [],
            'indent': ' ' * 4,
            'comment_prefix': '  #',
diff --git a/setup.py b/setup.py
index fbcb933e..381896ec 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.5',
       description='A Python utility / library to sort Python imports.',
       long_description=readme,
       author='Timothy Crosley',


Test patch:
diff --git a/test_isort.py b/test_isort.py
index bbd80c55..92764ed8 100644
--- a/test_isort.py
+++ b/test_isort.py
@@ -1304,7 +1304,7 @@ def test_include_trailing_comma():
     ).output
     assert test_output_wrap_single_import_vertical_indent == (
         "from third_party import (\n"
-        "    lib1\n"
+        "    lib1,\n"
         ")\n"
     )
 
@@ -1978,17 +1978,14 @@ def test_no_additional_lines_issue_358():
                        '    print_function,\n'
                        '    unicode_literals\n'
                        ')\n')
-    test_output = SortImports(file_contents=test_input, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT,
-                              line_length=20).output
+    test_output = SortImports(file_contents=test_input, multi_line_output=3, line_length=20).output
     assert test_output == expected_output
 
-    test_output = SortImports(file_contents=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT,
-                              line_length=20).output
+    test_output = SortImports(file_contents=test_output, multi_line_output=3, line_length=20).output
     assert test_output == expected_output
 
     for attempt in range(5):
-        test_output = SortImports(file_contents=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT,
-                                  line_length=20).output
+        test_output = SortImports(file_contents=test_output, multi_line_output=3, line_length=20).output
         assert test_output == expected_output
 
     test_input = ('"""This is a docstring"""\n'
@@ -2009,17 +2006,14 @@ def test_no_additional_lines_issue_358():
                        '    print_function,\n'
                        '    unicode_literals\n'
                        ')\n')
-    test_output = SortImports(file_contents=test_input, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT,
-                              line_length=20).output
+    test_output = SortImports(file_contents=test_input, multi_line_output=3, line_length=20).output
     assert test_output == expected_output
 
-    test_output = SortImports(file_contents=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT,
-                              line_length=20).output
+    test_output = SortImports(file_contents=test_output, multi_line_output=3, line_length=20).output
     assert test_output == expected_output
 
     for attempt in range(5):
-        test_output = SortImports(file_contents=test_output, multi_line_output=WrapModes.VERTICAL_HANGING_INDENT,
-                                  line_length=20).output
+        test_output = SortImports(file_contents=test_output, multi_line_output=3, line_length=20).output
         assert test_output == expected_output
 
 
@@ -2188,7 +2182,7 @@ def test_alias_using_paren_issue_466():
     expected_output = ('from django.db.backends.mysql.base import (\n'
                        '    DatabaseWrapper as MySQLDatabaseWrapper\n'
                        ')\n')
-    assert SortImports(file_contents=test_input, line_length=50, multi_line_output=WrapModes.VERTICAL_GRID_GROUPED,
+    assert SortImports(file_contents=test_input, line_length=50, multi_line_output=5,
                        use_parentheses=True).output == expected_output
 
 
@@ -2310,8 +2304,7 @@ def test_long_import_wrap_support_with_mode_2():
     """Test to ensure mode 2 still allows wrapped imports with slash"""
     test_input = ('from foobar.foobar.foobar.foobar import \\\n'
                   '    an_even_longer_function_name_over_80_characters\n')
-    assert SortImports(file_contents=test_input, multi_line_output=WrapModes.HANGING_INDENT,
-                       line_length=80).output == test_input
+    assert SortImports(file_contents=test_input, multi_line_output=2, line_length=80).output == test_input
 
 
 def test_pylint_comments_incorrectly_wrapped_issue_571():