From 3a04399a68bbe94e325fd416b50cb31061f2fad4 Mon Sep 17 00:00:00 2001 From: Dane Parin Date: Tue, 1 Sep 2026 13:22:09 +0700 Subject: [PATCH 1/4] Preserve trailing comments for aliased imports --- isort/output.py | 21 ++++++++++++++++----- isort/parse.py | 22 ++++++++-------------- tests/unit/test_regressions.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 19 deletions(-) diff --git a/isort/output.py b/isort/output.py index 1649afe5..0f79a701 100644 --- a/isort/output.py +++ b/isort/output.py @@ -649,11 +649,20 @@ def _with_from_imports_for_module( ) ) else: + # `combine_as_imports` with `force_single_line` stores the alias comment + # in `straight["module.base"]` (see parse.py). The combined + # `from_import` here is the alias string e.g. "the_function as some_function" + # so we need to look up via the base name. + base = from_import.split(" as ")[0] if " as " in from_import else from_import + per_alias_straight = parsed.categorized_comments["straight"].pop( + f"{module}.{base}", [] + ) single_import_line = with_comments( [ c for c in ( *comments, + *per_alias_straight, parsed.categorized_comments["nested"] .get(module, {}) .pop(from_import, None), @@ -754,12 +763,14 @@ def _with_from_imports_for_module( ): from_import_section.append(from_imports.pop(0)) - # If we are combining aliased imports we need to pop any associated comments. if config.combine_as_imports: - comments = [ - *comments, - *(parsed.categorized_comments["from"].pop(f"{module}.__combined_as__", ())), - ] + combined_as_comments: list[str] = [] + for imp in from_import_section: + base = imp.split(" as ")[0] + combined_as_comments.extend( + parsed.categorized_comments["straight"].pop(f"{module}.{base}", []) + ) + comments = [*comments, *combined_as_comments] grouped_from_import_statement = _build_grouped_from_imports( config=config, diff --git a/isort/parse.py b/isort/parse.py index 06299984..78ca6533 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -265,7 +265,6 @@ def _get_next_line() -> tuple[str, str | None]: if "as" in just_imports and (just_imports.index("as") + 1) < len(just_imports): straight_import = False while "as" in just_imports: - nested_module = None as_index = just_imports.index("as") if type_of_import == "from": nested_module = just_imports[as_index - 1] @@ -297,21 +296,16 @@ def _get_next_line() -> tuple[str, str | None]: as_map["straight"][module].append(as_name) if comments and attach_comments_to is None: - if nested_module and config.combine_as_imports: - attach_comments_to = categorized_comments["from"].setdefault( - f"{top_level_module}.__combined_as__", [] + if type_of_import == "from" or ( + config.remove_redundant_aliases and as_name == module.split(".")[-1] + ): + attach_comments_to = categorized_comments["straight"].setdefault( + module, [] ) else: - if type_of_import == "from" or ( - config.remove_redundant_aliases and as_name == module.split(".")[-1] - ): - attach_comments_to = categorized_comments["straight"].setdefault( - module, [] - ) - else: - attach_comments_to = categorized_comments["straight"].setdefault( - f"{module} as {as_name}", [] - ) + attach_comments_to = categorized_comments["straight"].setdefault( + f"{module} as {as_name}", [] + ) del just_imports[as_index : as_index + 2] if type_of_import == "from": diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index 4e9b619d..f3a7ef87 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -571,6 +571,37 @@ def test_combine_as_does_not_lose_comments_issue_1381(): assert "# type: ignore" in isort.code(test_input, combine_as_imports=True) +def test_combine_as_with_force_single_line_does_not_lose_comments_issue_2094(): + """Test to ensure isort doesn't lose trailing comments for aliased imports + when both combine_as_imports and force_single_line are enabled. + See: https://github.com/PyCQA/isort/issues/2094 + """ + import re # noqa: PLC0415 # local import, consistent with test_sort_reexports_output_is_black_stable_issue_2280 + + test_input = """from other_module import other_function # type: ignore [import] # pylint: disable=no-name-in-module +from some_module import the_function as some_function # type: ignore +from some_other_module import another_function as yet_another_function # type: ignore [import] # pylint: disable=no-name-in-module +""" + output = isort.code(test_input, combine_as_imports=True, force_single_line=True) + joined = output.replace("\\\n", " ") + assert re.search( + r"^from other_module import\s+other_function\s+# type: ignore \[import\]\s+# pylint: disable=no-name-in-module$", + joined, + re.MULTILINE, + ) + assert re.search( + r"^from some_module import the_function as some_function\s+# type: ignore$", + joined, + re.MULTILINE, + ) + assert re.search( + r"^from some_other_module import\s+another_function as\s+yet_another_function\s+# type: ignore \[import\]\s+# pylint: disable=no-name-in-module$", + joined, + re.MULTILINE, + ) + assert isort.code(output, combine_as_imports=True, force_single_line=True) == output + + def test_incorrect_grouping_when_comments_issue_1396(): """Test to ensure isort groups import correct independent of the comments present. See: https://github.com/pycqa/isort/issues/1396 From 4d38c438777f81ce437c7b72cabf2672a8059978 Mon Sep 17 00:00:00 2001 From: Dane Parin Date: Thu, 10 Sep 2026 17:24:12 +0700 Subject: [PATCH 2/4] Add per-alias comment coverage --- tests/unit/test_regressions.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index f3a7ef87..5c3a5b5c 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -602,6 +602,33 @@ def test_combine_as_with_force_single_line_does_not_lose_comments_issue_2094(): assert isort.code(output, combine_as_imports=True, force_single_line=True) == output +def test_combine_as_with_force_single_line_keeps_per_alias_comments_issue_2094(): + """Each alias from the same module keeps its own trailing comment. + + Pins the per-base keying at the core of this fix: comments must not swap + or duplicate across aliases of one module. + See: https://github.com/PyCQA/isort/issues/2094 + """ + import re # noqa: PLC0415 # local import, consistent with sibling test above + + test_input = """from some_module import the_function as some_function # type: ignore +from some_module import other_function as other_alias # noqa: F401 +""" + output = isort.code(test_input, combine_as_imports=True, force_single_line=True) + joined = output.replace("\\\n", " ") + assert re.search( + r"^from some_module import the_function as some_function\s+# type: ignore$", + joined, + re.MULTILINE, + ) + assert re.search( + r"^from some_module import other_function as other_alias\s+# noqa: F401$", + joined, + re.MULTILINE, + ) + assert isort.code(output, combine_as_imports=True, force_single_line=True) == output + + def test_incorrect_grouping_when_comments_issue_1396(): """Test to ensure isort groups import correct independent of the comments present. See: https://github.com/pycqa/isort/issues/1396 From 0ddf970215d2f3feb7addd43afae774e0180e019 Mon Sep 17 00:00:00 2001 From: Dane Parin Date: Fri, 11 Sep 2026 20:43:27 +0700 Subject: [PATCH 3/4] Update output.py --- isort/output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/output.py b/isort/output.py index 0f79a701..f7741803 100644 --- a/isort/output.py +++ b/isort/output.py @@ -651,7 +651,7 @@ def _with_from_imports_for_module( else: # `combine_as_imports` with `force_single_line` stores the alias comment # in `straight["module.base"]` (see parse.py). The combined - # `from_import` here is the alias string e.g. "the_function as some_function" + # `from_import` here is the alias string. For example, "the_function as some_function" # so we need to look up via the base name. base = from_import.split(" as ")[0] if " as " in from_import else from_import per_alias_straight = parsed.categorized_comments["straight"].pop( From 355b250bd45ff120699283da585c8febaa26b5bc Mon Sep 17 00:00:00 2001 From: Dane Parin Date: Sat, 12 Sep 2026 08:36:24 +0700 Subject: [PATCH 4/4] Revert comment style --- isort/output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/isort/output.py b/isort/output.py index f7741803..0f79a701 100644 --- a/isort/output.py +++ b/isort/output.py @@ -651,7 +651,7 @@ def _with_from_imports_for_module( else: # `combine_as_imports` with `force_single_line` stores the alias comment # in `straight["module.base"]` (see parse.py). The combined - # `from_import` here is the alias string. For example, "the_function as some_function" + # `from_import` here is the alias string e.g. "the_function as some_function" # so we need to look up via the base name. base = from_import.split(" as ")[0] if " as " in from_import else from_import per_alias_straight = parsed.categorized_comments["straight"].pop(