From 2ad61b4b3b5698a6798f718f634b1af84f2dc94a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9ni=20Gauffier=7E?= Date: Wed, 29 Dec 2021 20:17:53 +0100 Subject: [PATCH] Add magic trailing comma option --- docs/configuration/options.md | 11 +++++++++++ docs/configuration/profiles.md | 1 + isort/main.py | 6 ++++++ isort/output.py | 14 ++++++++++++-- isort/parse.py | 9 ++++++++- isort/settings.py | 1 + isort/wrap.py | 17 ++++++++++++----- tests/unit/test_isort.py | 16 ++++++++++++++++ tests/unit/test_parse.py | 1 + tests/unit/test_wrap.py | 3 +++ 10 files changed, 71 insertions(+), 8 deletions(-) diff --git a/docs/configuration/options.md b/docs/configuration/options.md index 527798a6b..56239d6f2 100644 --- a/docs/configuration/options.md +++ b/docs/configuration/options.md @@ -889,6 +889,17 @@ Includes a trailing comma on multi line imports that include parentheses. - --tc - --trailing-comma +## Split on Trailing Comma + +Split imports list followed by a trailing comma into VERTICAL_HANGING_INDENT mode. This follows Black style magic comma. + +**Type:** Bool +**Default:** `False` +**Config default:** `false` +**Python & Config File Name:** split_on_trailing_comma +**CLI Flags:** + +- --split-on-trailing-comma ## From First diff --git a/docs/configuration/profiles.md b/docs/configuration/profiles.md index a8e09dd25..56b10757a 100644 --- a/docs/configuration/profiles.md +++ b/docs/configuration/profiles.md @@ -16,6 +16,7 @@ To use any of the listed profiles, use `isort --profile PROFILE_NAME` from the c - **use_parentheses**: `True` - **ensure_newline_before_comments**: `True` - **line_length**: `88` + - **split_on_trailing_comma**: `True` #django diff --git a/isort/main.py b/isort/main.py index 40725a6a6..de3cb1c04 100644 --- a/isort/main.py +++ b/isort/main.py @@ -714,6 +714,12 @@ def _build_arg_parser() -> argparse.ArgumentParser: dest="star_first", action="store_true", ) + output_group.add_argument( + "--split-on-trailing-comma", + help="Split imports list followed by a trailing comma into VERTICAL_HANGING_INDENT mode", + dest="split_on_trailing_comma", + action="store_true", + ) section_group.add_argument( "--sd", diff --git a/isort/output.py b/isort/output.py index d049daf64..c59be936d 100644 --- a/isort/output.py +++ b/isort/output.py @@ -505,7 +505,17 @@ def _with_from_imports( ): do_multiline_reformat = True - if do_multiline_reformat: + if config.split_on_trailing_comma and module in parsed.trailing_commas: + import_statement = wrap.import_statement( + import_start=import_start, + from_imports=from_import_section, + comments=comments, + line_separator=parsed.line_separator, + config=config, + explode=True, + ) + + elif do_multiline_reformat: import_statement = wrap.import_statement( import_start=import_start, from_imports=from_import_section, @@ -530,7 +540,7 @@ def _with_from_imports( > config.line_length ): import_statement = other_import_statement - if not do_multiline_reformat and len(import_statement) > config.line_length: + elif len(import_statement) > config.line_length: import_statement = wrap.line(import_statement, parsed.line_separator, config) if import_statement: diff --git a/isort/parse.py b/isort/parse.py index 7fc6c8e65..c60938d87 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -2,7 +2,7 @@ from collections import OrderedDict, defaultdict from functools import partial from itertools import chain -from typing import TYPE_CHECKING, Any, Dict, List, NamedTuple, Optional, Tuple +from typing import TYPE_CHECKING, Any, Dict, List, NamedTuple, Optional, Set, Tuple from warnings import warn from . import place @@ -138,6 +138,7 @@ class ParsedContent(NamedTuple): line_separator: str sections: Any verbose_output: List[str] + trailing_commas: Set[str] def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedContent: @@ -176,6 +177,8 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte "above": {"straight": {}, "from": {}}, } + trailing_commas: Set[str] = set() + index = 0 import_index = -1 in_quote = "" @@ -515,6 +518,9 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte if comments and attach_comments_to is not None: attach_comments_to.extend(comments) + + if "," in import_string.split(just_imports[-1])[-1]: + trailing_commas.add(import_from) else: if comments and attach_comments_to is not None: attach_comments_to.extend(comments) @@ -587,4 +593,5 @@ def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedConte line_separator=line_separator, sections=config.sections, verbose_output=verbose_output, + trailing_commas=trailing_commas, ) diff --git a/isort/settings.py b/isort/settings.py index f13afade5..f6fd6f51a 100644 --- a/isort/settings.py +++ b/isort/settings.py @@ -243,6 +243,7 @@ class _Config: format_success: str = "{success}: {message}" sort_order: str = "natural" sort_reexports: bool = False + split_on_trailing_comma: bool = False def __post_init__(self) -> None: py_version = self.py_version diff --git a/isort/wrap.py b/isort/wrap.py index 5fb4631f7..eb4c24832 100644 --- a/isort/wrap.py +++ b/isort/wrap.py @@ -4,7 +4,7 @@ from .settings import DEFAULT_CONFIG, Config from .wrap_modes import WrapModes as Modes -from .wrap_modes import formatter_from_string +from .wrap_modes import formatter_from_string, vertical_hanging_indent def import_statement( @@ -14,12 +14,19 @@ def import_statement( line_separator: str = "\n", config: Config = DEFAULT_CONFIG, multi_line_output: Optional[Modes] = None, + explode: bool = False, ) -> str: """Returns a multi-line wrapped form of the provided from import statement.""" - formatter = formatter_from_string((multi_line_output or config.multi_line_output).name) + if explode: + formatter = vertical_hanging_indent + line_length = 1 + include_trailing_comma = True + else: + formatter = formatter_from_string((multi_line_output or config.multi_line_output).name) + line_length = config.wrap_length or config.line_length + include_trailing_comma = config.include_trailing_comma dynamic_indent = " " * (len(import_start) + 1) indent = config.indent - line_length = config.wrap_length or config.line_length statement = formatter( statement=import_start, imports=copy.copy(from_imports), @@ -29,7 +36,7 @@ def import_statement( comments=comments, line_separator=line_separator, comment_prefix=config.comment_prefix, - include_trailing_comma=config.include_trailing_comma, + include_trailing_comma=include_trailing_comma, remove_comments=config.ignore_comments, ) if config.balanced_wrapping: @@ -52,7 +59,7 @@ def import_statement( comments=comments, line_separator=line_separator, comment_prefix=config.comment_prefix, - include_trailing_comma=config.include_trailing_comma, + include_trailing_comma=include_trailing_comma, remove_comments=config.ignore_comments, ) lines = new_import_statement.split(line_separator) diff --git a/tests/unit/test_isort.py b/tests/unit/test_isort.py index 293b99bec..4b86f7ec5 100644 --- a/tests/unit/test_isort.py +++ b/tests/unit/test_isort.py @@ -5562,3 +5562,19 @@ def seekable(self): test_input = NonSeekableTestStream("import m2\n" "import m1\n" "not_import = 7") identified_imports = list(map(str, api.find_imports_in_stream(test_input))) assert identified_imports == [":1 import m2", ":2 import m1"] + + +def test_split_on_trailing_comma() -> None: + test_input = "from lib import (a, b, c,)" + expected_output = """from lib import ( + a, + b, + c, +) +""" + + output = isort.code(test_input, split_on_trailing_comma=True) + assert output == expected_output + + output = isort.code(expected_output, split_on_trailing_comma=True) + assert output == expected_output diff --git a/tests/unit/test_parse.py b/tests/unit/test_parse.py index 0becac900..1a66a5cd6 100644 --- a/tests/unit/test_parse.py +++ b/tests/unit/test_parse.py @@ -38,6 +38,7 @@ def test_file_contents(): _, _, _, + _, ) = parse.file_contents(TEST_CONTENTS, config=Config(default_section="")) assert "\n".join(in_lines) == TEST_CONTENTS assert "import" not in "\n".join(out_lines) diff --git a/tests/unit/test_wrap.py b/tests/unit/test_wrap.py index 2b8ec6fad..2e35951fe 100644 --- a/tests/unit/test_wrap.py +++ b/tests/unit/test_wrap.py @@ -13,3 +13,6 @@ def test_import_statement(): == """from long_import (verylong, verylong, verylong, verylong, verylong, verylong, verylong, verylong, verylong, verylong)""" ) + assert wrap.import_statement("from x import ", ["y", "z"], [], explode=True) == ( + "from x import (\n y,\n z,\n)" + )