Skip to content

Commit 57f1638

Browse files
committed
Restructure code and more tests
1 parent 67c612c commit 57f1638

2 files changed

Lines changed: 122 additions & 112 deletions

File tree

isort/wrap.py

Lines changed: 89 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -70,101 +70,101 @@ def import_statement(
7070

7171
def line(content: str, line_separator: str, config: Config = DEFAULT_CONFIG) -> str:
7272
"""Returns a line wrapped to the specified line-length, if possible."""
73-
# A ``from ... import *`` statement cannot use parenthesis-based wrapping
74-
# because the wildcard ``*`` has no valid continuation in that mode.
75-
# Use a backslash continuation instead so the statement is split across
76-
# lines while remaining valid Python.
73+
if len(content) <= config.line_length:
74+
return content
75+
76+
wrap_mode = config.multi_line_output
77+
if wrap_mode is Modes.NOQA:
78+
if "# NOQA" not in content:
79+
return f"{content}{config.comment_prefix} NOQA"
80+
return content
81+
82+
line_without_comment = content
83+
comment = None
84+
if "#" in content:
85+
line_without_comment, comment = content.split("#", 1)
86+
87+
# A ``from ... import *`` / ``from ... cimport *`` statement cannot use
88+
# parenthesis-based wrapping because the wildcard ``*`` has no valid
89+
# continuation in that mode. Use a backslash continuation instead so the
90+
# statement is split across lines while remaining valid Python.
7791
# See https://github.com/PyCQA/isort/issues/2267
78-
bare = content.split("#", 1)[0].rstrip()
79-
if bare.startswith("from ") and bare.endswith(" import *"):
80-
if len(content) <= config.line_length:
81-
return content
82-
line_without_comment, _, comment = content.partition("#")
83-
prefix = line_without_comment.rstrip()
92+
if line_without_comment.rstrip().endswith("*"):
93+
prefix, keyword, _ = line_without_comment.rstrip().rsplit(" ", 2)
8494
comment_suffix = f" #{comment}" if comment else ""
85-
module_path = prefix[: -len(" import *")]
86-
return f"{module_path} import \\{line_separator}{config.indent}*{comment_suffix}"
87-
wrap_mode = config.multi_line_output
88-
if len(content) > config.line_length and wrap_mode != Modes.NOQA:
89-
line_without_comment = content
90-
comment = None
91-
if "#" in content:
92-
line_without_comment, comment = content.split("#", 1)
93-
for splitter in ("import ", "cimport ", ".", "as "):
94-
exp = r"\b" + re.escape(splitter) + r"\b"
95-
if re.search(exp, line_without_comment) and not line_without_comment.strip().startswith(
96-
splitter
97-
):
98-
line_parts = re.split(exp, line_without_comment)
99-
_is_vertical_mode = wrap_mode in (
100-
Modes.VERTICAL_HANGING_INDENT,
101-
Modes.VERTICAL_GRID_GROUPED,
95+
return f"{prefix} {keyword} \\{line_separator}{config.indent}*{comment_suffix}"
96+
97+
for splitter in ("import ", "cimport ", ".", "as "):
98+
exp = r"\b" + re.escape(splitter) + r"\b"
99+
if re.search(exp, line_without_comment) and not line_without_comment.strip().startswith(
100+
splitter
101+
):
102+
line_parts = re.split(exp, line_without_comment)
103+
_is_vertical_mode = wrap_mode in (
104+
Modes.VERTICAL_HANGING_INDENT,
105+
Modes.VERTICAL_GRID_GROUPED,
106+
)
107+
# Determine whether the comment should be hoisted to the opening
108+
# parenthesis line rather than embedded in the import line.
109+
# This happens for noqa comments (when use_parentheses is True)
110+
# and for all comments in vertical hanging modes (when
111+
# use_parentheses is False), so that multi_line_output=3/5 is
112+
# respected even without an explicit use_parentheses=True setting.
113+
_hoist_comment_to_paren = comment and (
114+
(config.use_parentheses and "noqa" in comment)
115+
or (_is_vertical_mode and not config.use_parentheses)
116+
)
117+
if comment and not _hoist_comment_to_paren:
118+
_comma_maybe = (
119+
","
120+
if (
121+
config.include_trailing_comma
122+
and config.use_parentheses
123+
and not line_without_comment.rstrip().endswith(",")
124+
)
125+
else ""
102126
)
103-
# Determine whether the comment should be hoisted to the opening
104-
# parenthesis line rather than embedded in the import line.
105-
# This happens for noqa comments (when use_parentheses is True)
106-
# and for all comments in vertical hanging modes (when
107-
# use_parentheses is False), so that multi_line_output=3/5 is
108-
# respected even without an explicit use_parentheses=True setting.
109-
_hoist_comment_to_paren = comment and (
110-
(config.use_parentheses and "noqa" in comment)
111-
or (_is_vertical_mode and not config.use_parentheses)
127+
line_parts[-1] = (
128+
f"{line_parts[-1].strip()}{_comma_maybe}{config.comment_prefix}{comment}"
112129
)
113-
if comment and not _hoist_comment_to_paren:
114-
_comma_maybe = (
115-
","
116-
if (
117-
config.include_trailing_comma
118-
and config.use_parentheses
119-
and not line_without_comment.rstrip().endswith(",")
120-
)
121-
else ""
122-
)
123-
line_parts[-1] = (
124-
f"{line_parts[-1].strip()}{_comma_maybe}{config.comment_prefix}{comment}"
125-
)
126-
next_line = []
127-
while (len(content) + 2) > (
128-
config.wrap_length or config.line_length
129-
) and line_parts:
130-
next_line.append(line_parts.pop())
131-
content = splitter.join(line_parts)
132-
if not content:
133-
content = next_line.pop()
130+
next_line = []
131+
while (len(content) + 2) > (config.wrap_length or config.line_length) and line_parts:
132+
next_line.append(line_parts.pop())
133+
content = splitter.join(line_parts)
134+
if not content:
135+
content = next_line.pop()
134136

135-
cont_line = _wrap_line(
136-
config.indent + splitter.join(next_line).lstrip(),
137-
line_separator,
138-
config,
139-
)
140-
if config.use_parentheses or _is_vertical_mode:
141-
if splitter == "as ":
142-
output = f"{content}{splitter}{cont_line.lstrip()}"
143-
else:
144-
_comma = "," if config.include_trailing_comma and not comment else ""
137+
cont_line = _wrap_line(
138+
config.indent + splitter.join(next_line).lstrip(),
139+
line_separator,
140+
config,
141+
)
142+
if config.use_parentheses or _is_vertical_mode:
143+
if splitter == "as ":
144+
output = f"{content}{splitter}{cont_line.lstrip()}"
145+
else:
146+
_comma = "," if config.include_trailing_comma and not comment else ""
145147

146-
if _is_vertical_mode:
147-
_separator = line_separator
148-
else:
149-
_separator = ""
150-
noqa_comment = ""
151-
if _hoist_comment_to_paren:
152-
noqa_comment = f"{config.comment_prefix}{comment}"
153-
cont_line = cont_line.rstrip()
154-
_comma = "," if config.include_trailing_comma else ""
155-
output = (
156-
f"{content}{splitter}({noqa_comment}"
157-
f"{line_separator}{cont_line}{_comma}{_separator})"
158-
)
159-
lines = output.split(line_separator)
160-
if config.comment_prefix in lines[-1] and lines[-1].endswith(")"):
161-
content, comment = lines[-1].split(config.comment_prefix, 1)
162-
lines[-1] = content + ")" + config.comment_prefix + comment[:-1]
163-
output = line_separator.join(lines)
164-
return output
165-
return f"{content}{splitter}\\{line_separator}{cont_line}"
166-
elif len(content) > config.line_length and wrap_mode == Modes.NOQA and "# NOQA" not in content:
167-
return f"{content}{config.comment_prefix} NOQA"
148+
if _is_vertical_mode:
149+
_separator = line_separator
150+
else:
151+
_separator = ""
152+
noqa_comment = ""
153+
if _hoist_comment_to_paren:
154+
noqa_comment = f"{config.comment_prefix}{comment}"
155+
cont_line = cont_line.rstrip()
156+
_comma = "," if config.include_trailing_comma else ""
157+
output = (
158+
f"{content}{splitter}({noqa_comment}"
159+
f"{line_separator}{cont_line}{_comma}{_separator})"
160+
)
161+
lines = output.split(line_separator)
162+
if config.comment_prefix in lines[-1] and lines[-1].endswith(")"):
163+
content, comment = lines[-1].split(config.comment_prefix, 1)
164+
lines[-1] = content + ")" + config.comment_prefix + comment[:-1]
165+
output = line_separator.join(lines)
166+
return output
167+
return f"{content}{splitter}\\{line_separator}{cont_line}"
168168

169169
return content
170170

tests/unit/test_wrap.py

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -50,36 +50,46 @@ def test_line__comment_with_brackets__expects_unchanged_comment(multi_line_outpu
5050
assert wrap.line(content=content, line_separator="\n", config=config) == expected
5151

5252

53-
def test_line__star_import_wrapped_with_backslash():
54-
# A ``from ... import *`` statement cannot use parenthesis-based wrapping,
55-
# so isort falls back to a backslash continuation when the line exceeds
56-
# ``line_length``. See issue #2267.
57-
content = "from very.very.very.very.very.very.very.very.very.very.very.long.line import *"
53+
def test_line_star_import_wrapped_with_backslash() -> None:
54+
"""Star imports cannot use parenthesis-based wrapping, so should use backslashes.
55+
56+
See issue #2267.
57+
"""
58+
content = "from very.very.very.very.very.very.very.very.very.long.line import *"
59+
expected = "from very.very.very.very.very.very.very.very.very.long.line import \\\n *"
5860
config = Config(line_length=20)
59-
expected = (
60-
"from very.very.very.very.very.very.very.very.very.very.very.long.line import \\"
61-
"\n *"
62-
)
6361
assert wrap.line(content=content, line_separator="\n", config=config) == expected
6462

6563

66-
def test_line__star_import_with_comment_wrapped_with_backslash():
67-
content = (
68-
"from very.very.very.very.very.very.very.very.very.very.very.long.line import *"
69-
" # noqa: F401"
70-
)
64+
def test_line_star_cimport_wrapped_with_backslash() -> None:
65+
"""Star cimports should also use backslashes."""
66+
content = "from very.very.very.very.very.very.very.very.very.long.line cimport *"
67+
expected = "from very.very.very.very.very.very.very.very.very.long.line cimport \\\n *"
68+
config = Config(line_length=20)
69+
assert wrap.line(content=content, line_separator="\n", config=config) == expected
70+
71+
72+
def test_line_star_import_with_comment_wrapped_with_backslash() -> None:
73+
"""When falling back to backslashes for start imports, comments should be preserved."""
74+
content = "from very.very.very.very.very.very.very.very.very.long.line import * # noqa: F401"
7175
config = Config(line_length=20)
7276
expected = (
73-
"from very.very.very.very.very.very.very.very.very.very.very.long.line import \\"
74-
"\n * # noqa: F401"
77+
"from very.very.very.very.very.very.very.very.very.long.line import \\\n * # noqa: F401"
7578
)
7679
assert wrap.line(content=content, line_separator="\n", config=config) == expected
7780

7881

79-
def test_star_import_wrapped_end_to_end():
80-
source = "from very.very.very.very.very.very.very.very.very.very.very.very.long.line import *\n"
81-
result = code(source, line_length=20, force_single_line=True)
82-
# The wildcard import should be split with a backslash continuation.
83-
lines = result.strip().splitlines()
84-
assert lines[0].rstrip().endswith("import \\")
85-
assert lines[1].strip() == "*"
82+
def test_line_star_import_in_noqa_mode_is_not_backslash_wrapped() -> None:
83+
"""NOQA mode should prevent backslashes getting inserted for too long star imports."""
84+
content = "from very.very.very.very.very.very.very.very.very.very.very.long.line import *"
85+
config = Config(line_length=20, multi_line_output=WrapModes.NOQA)
86+
assert wrap.line(content=content, line_separator="\n", config=config) == (
87+
"from very.very.very.very.very.very.very.very.very.very.very.long.line import * # NOQA"
88+
)
89+
90+
91+
def test_star_import_wrapped_end_to_end() -> None:
92+
"""New lines should be preserved at the end of too long start imports."""
93+
source = "from very.very.very.very.very.very.very.very.very.long.line import *\n"
94+
expected = "from very.very.very.very.very.very.very.very.very.long.line import \\\n *\n"
95+
assert code(source, line_length=20, force_single_line=True) == expected

0 commit comments

Comments
 (0)