Skip to content

Commit 0e3cd7e

Browse files
committed
fix: keep force_single body comments on first emitted as-import
Pending opening/body comments now attach to the first emitted force_single line (plain or as). Single-line inject merges existing nested trailing comments instead of clobbering them. Signed-off-by: Alex Chen <l46983284@gmail.com>
1 parent 0377e89 commit 0e3cd7e

2 files changed

Lines changed: 75 additions & 31 deletions

File tree

isort/output.py

Lines changed: 50 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from . import _parse_utils, parse, sorting, wrap, wrap_modes
1010
from .comments import add_to_line as with_comments
11+
from .comments import parse as parse_comment
1112
from .identify import STATEMENT_DECLARATIONS
1213
from .place import module_with_reason
1314
from .settings import DEFAULT_CONFIG, Config
@@ -320,10 +321,28 @@ def _inject_from_body_comments(
320321
return line_separator.join(lines)
321322

322323
# Single-line / no-paren fallback: main-compatible fold onto the statement.
324+
# Merge any trailing comment already on the statement (nested inline) after body
325+
# comments so we keep main's ``# body; nested`` shape instead of clobbering.
326+
if ignore_comments:
327+
return with_comments(
328+
body_comments,
329+
import_statement,
330+
removed=True,
331+
comment_prefix=comment_prefix,
332+
)
333+
_base, existing_comment = parse_comment(import_statement)
334+
# Drop spacing that used to precede an inline comment so re-attach is stable.
335+
_base = _base.rstrip()
336+
merged = list(body_comments)
337+
if existing_comment:
338+
for part in existing_comment.split(";"):
339+
part = part.strip()
340+
if part and part not in merged:
341+
merged.append(part)
323342
return with_comments(
324-
body_comments,
325-
import_statement,
326-
removed=ignore_comments,
343+
merged,
344+
_base,
345+
removed=False,
327346
comment_prefix=comment_prefix,
328347
)
329348

@@ -434,15 +453,19 @@ def _with_from_imports(
434453
only_show_as_imports = True
435454
elif config.force_single_line and module not in config.single_line_exclusions:
436455
import_statement = ""
437-
# Preserve comment-only members on the first single-line import
438-
# (matches pre-#1852 main behaviour for force_single_line).
456+
# Pending comments (opening + body) must land on the first *emitted*
457+
# single-line statement. As-only modules never emit the bare name, so
458+
# folding into ``comments`` on that non-emitted line drops body text.
459+
pending_comments: list[str] = list(comments or [])
439460
if body_comments and not config.ignore_comments:
440-
comments = list(comments or []) + body_comments
461+
pending_comments.extend(body_comments)
441462
body_comments = []
463+
comments = None
442464
while from_imports:
443465
from_import = from_imports.pop(0)
466+
line_comments = pending_comments or None
444467
single_import_line = with_comments(
445-
comments,
468+
line_comments,
446469
import_start + from_import,
447470
removed=config.ignore_comments,
448471
comment_prefix=config.comment_prefix,
@@ -453,52 +476,48 @@ def _with_from_imports(
453476
if comment is not None:
454477
comment_text = f" {comment}" if comment else ""
455478
single_import_line += (
456-
f"{(comments and ';') or config.comment_prefix}{comment_text}"
479+
f"{(line_comments and ';') or config.comment_prefix}{comment_text}"
457480
)
458481
if from_import in as_imports:
482+
emitted_plain = False
459483
if (
460484
parsed.imports[section][import_key][module][from_import]
461485
and not only_show_as_imports
462486
):
463487
output.append(
464488
wrap.line(single_import_line, parsed.line_separator, config)
465489
)
466-
from_comments = parsed.categorized_comments["straight"].get(
467-
f"{module}.{from_import}"
490+
pending_comments = []
491+
emitted_plain = True
492+
from_comments = list(
493+
parsed.categorized_comments["straight"].get(f"{module}.{from_import}")
494+
or []
468495
)
469-
470-
if not config.only_sections:
471-
output.extend(
472-
wrap.line(
473-
with_comments(
474-
from_comments,
475-
import_start + as_import,
476-
removed=config.ignore_comments,
477-
comment_prefix=config.comment_prefix,
478-
),
479-
parsed.line_separator,
480-
config,
481-
)
482-
for as_import in sorting.sort(config, as_imports[from_import])
483-
)
484-
485-
else:
486-
output.extend(
496+
as_import_names = (
497+
sorting.sort(config, as_imports[from_import])
498+
if not config.only_sections
499+
else list(as_imports[from_import])
500+
)
501+
for index, as_import in enumerate(as_import_names):
502+
as_line_comments = list(from_comments) if index == 0 else []
503+
if index == 0 and pending_comments and not emitted_plain:
504+
as_line_comments = pending_comments + as_line_comments
505+
pending_comments = []
506+
output.append(
487507
wrap.line(
488508
with_comments(
489-
from_comments,
509+
as_line_comments or None,
490510
import_start + as_import,
491511
removed=config.ignore_comments,
492512
comment_prefix=config.comment_prefix,
493513
),
494514
parsed.line_separator,
495515
config,
496516
)
497-
for as_import in as_imports[from_import]
498517
)
499518
else:
500519
output.append(wrap.line(single_import_line, parsed.line_separator, config))
501-
comments = None
520+
pending_comments = []
502521
else:
503522
# Tracks whether any aliased imports were emitted before the grouped
504523
# non-aliased imports in this pass of the outer loop. When True it

tests/unit/test_regressions.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2573,3 +2573,28 @@ def test_parse_from_body_comments_issue_1852():
25732573

25742574
assert parsed.categorized_comments["from_body"] == {"foo": ["disabled"]}
25752575
assert parsed.categorized_comments["nested"] == {"foo": {"beta": "nested"}}
2576+
2577+
2578+
def test_force_single_line_as_import_keeps_body_comment_issue_1852():
2579+
"""force_single_line must keep body comments on the first emitted as-import."""
2580+
to_sort = "from foo import (\n # disabled\n alpha as a,\n)\n"
2581+
expected = "from foo import alpha as a # disabled\n"
2582+
first = isort.code(to_sort, force_single_line=True)
2583+
assert first == expected
2584+
assert isort.code(first, force_single_line=True) == first
2585+
2586+
mixed = "from foo import (\n alpha as a,\n # disabled\n beta,\n)\n"
2587+
mixed_out = isort.code(mixed, force_single_line=True)
2588+
assert mixed_out == ("from foo import alpha as a # disabled\nfrom foo import beta\n")
2589+
assert isort.code(mixed_out, force_single_line=True) == mixed_out
2590+
2591+
2592+
def test_black_nested_inline_and_body_comment_merge_issue_1852():
2593+
"""Body comments folded onto a nested-comment line keep both texts."""
2594+
to_sort = "from foo import (\n alpha, # a\n beta, # b\n # disabled\n)\n"
2595+
out = isort.code(to_sort, profile="black")
2596+
assert "alpha # a" in out
2597+
assert "disabled" in out
2598+
assert "# b" in out or "; b" in out or out.endswith("b\n") or " b" in out
2599+
assert "b" in out.split("beta", 1)[1]
2600+
assert isort.code(out, profile="black") == out

0 commit comments

Comments
 (0)