Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions isort/wrap_modes.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ def _hanging_indent_end_line(line: str) -> str:
return line + "\\"


def _add_syntax(statement: str, suffix: str, line_separator: str) -> str:
# Appended after a trailing comment, punctuation is commented out instead of emitted.
head, separator, last_line = statement.rpartition(line_separator)
code, comment_start, comment = last_line.partition("#")
if not comment_start:
return statement + suffix
spacing = code[len(code.rstrip()) :] or " "
return f"{head}{separator}{code.rstrip()}{suffix}{spacing}{comment_start}{comment}"


@_wrap_mode
def hanging_indent(**interface: Any) -> str:
if not interface["imports"]:
Expand Down Expand Up @@ -347,17 +357,25 @@ def hanging_indent_with_parentheses(**interface: Any) -> str:
current_line = next_statement.split(interface["line_separator"])[-1]
if len(current_line) > line_length_limit:
next_statement = (
isort.comments.add_to_line(
interface["comments"],
interface["statement"] + ",",
removed=interface["remove_comments"],
comment_prefix=interface["comment_prefix"],
_add_syntax(
isort.comments.add_to_line(
interface["comments"],
interface["statement"],
removed=interface["remove_comments"],
comment_prefix=interface["comment_prefix"],
),
",",
interface["line_separator"],
)
+ f"{interface['line_separator']}{interface['indent']}{next_import}"
)
interface["comments"] = []
interface["statement"] = next_statement
return f"{interface['statement']}{',' if interface['include_trailing_comma'] else ''})"
if interface["include_trailing_comma"]:
interface["statement"] = _add_syntax(
interface["statement"], ",", interface["line_separator"]
)
return _add_syntax(interface["statement"], ")", interface["line_separator"])


@_wrap_mode
Expand Down
48 changes: 48 additions & 0 deletions tests/unit/test_regressions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""A growing set of tests designed to ensure isort doesn't have regressions in new versions"""

import ast
from io import BytesIO, StringIO, TextIOWrapper

import pytest
Expand All @@ -8,6 +9,7 @@
import isort.sections
from isort.core import STRING_PREFIXES
from isort.main import main
from isort.wrap_modes import WrapModes


def test_isort_duplicating_comments_issue_1264():
Expand Down Expand Up @@ -2489,3 +2491,49 @@ def test_add_import_keeps_a_prefixed_module_docstring_first_issue_1893():
docstring = f"{cased}{quote}module docstring\n{quote}\n"
source = docstring + "import a\n"
assert isort.code(source, add_imports=["import a"]) == source, cased + quote


def test_hanging_indent_with_parentheses_keeps_syntax_out_of_trailing_comments():
"""``multi_line_output=10`` must not append a comma or the closing parenthesis after a
trailing comment, which silently rewrote valid code into code that no longer parses.

The mode keeps the comment on the import line (its siblings hoist it to the ``(`` line),
then appended punctuation to that line unconditionally. The closing ``)`` landed inside
the comment, and a wrapping comma was consumed by the comment re-splice in
``comments.add_to_line`` -- so the output lost a separator it never got back.
"""
# Closing parenthesis captured by the comment: "'(' was never closed".
captured_paren = isort.code(
"from a import b, c # trailing\nfrom a import d\n",
multi_line_output=10,
line_length=40,
force_grid_wrap=2,
)
assert ast.parse(captured_paren)
assert captured_paren == "from a import (b, c, d) # trailing\n"

# Comma eaten at a wrap point: the names either side of it merged into "eta gamma".
lost_comma = isort.code(
"from mypkg.submodule import alpha, beta, gamma, delta, epsilon, zeta, eta # noqa: F401\n",
multi_line_output=10,
line_length=79,
)
assert ast.parse(lost_comma)
assert "eta," in lost_comma

# No wrap mode may turn parsing code into non-parsing code.
for mode in WrapModes:
for line_length in (40, 79, 88):
for source in (
"from a import b, c # trailing\nfrom a import d\n",
"from mypkg.submodule import alpha, beta, gamma, delta, epsilon, zeta # noqa: F401\n",
"from mypkg.sub import alpha, beta, gamma, delta, epsilon # type: ignore\n",
):
for trailing_comma in (False, True):
output = isort.code(
source,
multi_line_output=mode.value,
line_length=line_length,
include_trailing_comma=trailing_comma,
)
ast.parse(output) # must never raise