Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add magic trailing comma option #1876

Merged
merged 3 commits into from May 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/configuration/options.md
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions docs/configuration/profiles.md
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions isort/main.py
Expand Up @@ -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",
Expand Down
14 changes: 12 additions & 2 deletions isort/output.py
Expand Up @@ -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,
Expand All @@ -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:
Expand Down
9 changes: 8 additions & 1 deletion isort/parse.py
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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 = ""
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
)
1 change: 1 addition & 0 deletions isort/settings.py
Expand Up @@ -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
Expand Down
17 changes: 12 additions & 5 deletions isort/wrap.py
Expand Up @@ -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(
Expand All @@ -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),
Expand All @@ -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:
Expand All @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions tests/unit/test_isort.py
Expand Up @@ -5564,6 +5564,22 @@ def seekable(self):
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


def test_infinite_loop_in_unmatched_parenthesis() -> None:
test_input = "from os import ("

Expand Down
1 change: 1 addition & 0 deletions tests/unit/test_parse.py
Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions tests/unit/test_wrap.py
Expand Up @@ -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)"
)