From 34c6e31d2b2a285cda68eaba276f1b867dd5e844 Mon Sep 17 00:00:00 2001 From: Sarath Francis Date: Fri, 26 Jun 2026 02:58:39 -0400 Subject: [PATCH] Keep aliased import when the plain name carries a comment When a name is imported both plainly and with an alias, and the plain import has a trailing comment, the aliased form was silently dropped if another member of the same module sorted ahead of it. The leading-alias loop only emits an aliased name while it sits at the front of the module group, so it never reached a name that a sibling sorted before. The later "this name has its own comment" pass then printed only the plain name and removed it from the group, taking the alias with it -- and the result no longer re-sorted cleanly. Leave a commented name that also has an alias for the alias loop instead of consuming it here. --- isort/output.py | 15 +++++++++++++++ tests/unit/test_regressions.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/isort/output.py b/isort/output.py index ada89f75..888c468e 100644 --- a/isort/output.py +++ b/isort/output.py @@ -550,6 +550,21 @@ def _with_from_imports( comment = ( parsed.categorized_comments["nested"].get(module, {}).pop(from_import, None) ) + if ( + comment is not None + and from_import in as_imports + and config.multi_line_output != wrap.Modes.NOQA # type: ignore[attr-defined] # noqa: E501 + ): + # This name also carries an alias. The leading-alias loop above emits + # the plain name (with this comment) together with its alias on a later + # pass of the outer ``while``. Consuming it here would print only the + # plain name and silently drop the alias, so put the comment back and + # leave the name for that loop. (The NOQA wrap mode appends its own + # per-line suppression and keeps its existing handling.) + parsed.categorized_comments["nested"].setdefault(module, {})[ + from_import + ] = comment + continue if comment is not None: # If the comment is a noqa and hanging indent wrapping is used, # keep the name in the main list and hoist the comment to the statement. diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index f843ff84..ad915437 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -2117,3 +2117,34 @@ def test_noqa_wrap_mode_does_not_accumulate_spaces_with_as_import(): first_pass, multi_line_output=7, force_single_line=True, line_length=40 ) assert second_pass == first_pass + + +def test_isort_does_not_drop_aliased_import_when_plain_name_has_a_comment(): + """A name imported both plainly and with an alias must keep its alias when the plain + import carries a trailing comment and another member sorts ahead of it. + + With default settings ``from x import m`` (commented) and ``from x import m as z`` are + combined into one module group. The aliased ``m as z`` is only emitted by the loop that + walks the *leading* aliased names, so it is reached only while ``m`` sits at the front of + the group. When a sibling such as ``aaa`` sorts before ``m`` that loop never sees ``m``; + the later "name has its own comment" pass then printed ``from x import m # c`` and + removed ``m`` from the group, so ``m as z`` was silently dropped (and the output no longer + re-sorted cleanly). The comment-bearing names that also have an alias must be left for the + alias loop instead of being consumed here. + """ + to_sort = "from x import aaa\nfrom x import m # c\nfrom x import m as z\n" + expected = "from x import aaa\nfrom x import m # c\nfrom x import m as z\n" + + first_pass = isort.code(to_sort) + assert first_pass == expected + assert "m as z" in first_pass + + # The result must already be a fixpoint - the dropped alias previously only surfaced on + # the second run. + assert isort.code(first_pass) == first_pass + + # The same holds for a relative (local-folder) import. + relative = "from . import bar, one\nfrom . import one as zzz # NOQA\n" + relative_sorted = isort.code(relative) + assert "one as zzz" in relative_sorted + assert isort.code(relative_sorted) == relative_sorted