From 41f0602a8ea04b938c29eebd73051ee6b8dd6e1b Mon Sep 17 00:00:00 2001 From: urayoru Date: Sat, 27 Jun 2026 16:11:19 +0800 Subject: [PATCH] Fix NOQA not added to long imports in several output paths In NOQA wrap mode (multi_line_output=7) isort appends `# NOQA` to imports that cannot fit on one line. Several import output paths bypassed `wrap.line()` or called it before comments were attached, so the line-length check never saw the full line and `# NOQA` was not added. Four paths were affected: 1. force_single_line as-imports (L404-432): `with_comments()` wrapped `wrap.line(...)` instead of the reverse, so comments were attached after the length check. Swap the order to `wrap.line(with_comments(...))`. Before (line_length=40): from m import func as alias # type: ignore After: from m import func as alias # type: ignore # NOQA 2. as-imports with opening-line comments + use_parentheses (L523-528): `wrap.line()` ran on the import without opening comments, then comments were appended afterward. Re-run `wrap.line()` in NOQA mode after the comment is attached. Before (line_length=40): from m import func as alias # type: ignore # pylint: disable=... After: from m import func as alias # type: ignore # pylint: disable=... # NOQA 3. combine_straight_imports (L725-736): the combined line was appended directly without going through `wrap.line()`. Route it through `wrap.line()`. Before (line_length=40): import a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p After: import a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p # NOQA 4. straight imports (L755-767): each import was emitted via a generator that only called `with_comments()`, never `wrap.line()`. Convert to an explicit loop and wrap each line with `wrap.line()` only in NOQA mode, to avoid changing wrapping behaviour in other modes. Before (line_length=40): import aaaa...long...module as bbbb...long...alias After: import aaaa...long...module as bbbb...long...alias # NOQA Fixes #2093. --- isort/output.py | 47 ++++++++++-------- tests/unit/test_regressions.py | 87 ++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 19 deletions(-) diff --git a/isort/output.py b/isort/output.py index ada89f75..bfbaee37 100644 --- a/isort/output.py +++ b/isort/output.py @@ -403,26 +403,30 @@ def _with_from_imports( if not config.only_sections: output.extend( - with_comments( - from_comments, - wrap.line( - import_start + as_import, parsed.line_separator, config + wrap.line( + with_comments( + from_comments, + import_start + as_import, + removed=config.ignore_comments, + comment_prefix=config.comment_prefix, ), - removed=config.ignore_comments, - comment_prefix=config.comment_prefix, + parsed.line_separator, + config, ) for as_import in sorting.sort(config, as_imports[from_import]) ) else: output.extend( - with_comments( - from_comments, - wrap.line( - import_start + as_import, parsed.line_separator, config + wrap.line( + with_comments( + from_comments, + import_start + as_import, + removed=config.ignore_comments, + comment_prefix=config.comment_prefix, ), - removed=config.ignore_comments, - comment_prefix=config.comment_prefix, + parsed.line_separator, + config, ) for as_import in as_imports[from_import] ) @@ -518,6 +522,8 @@ def _with_from_imports( ) if opening_comment: lines[0] += opening_comment + if config.multi_line_output == wrap.Modes.NOQA: # type: ignore[attr-defined] # noqa: E501 + lines[0] = wrap.line(lines[0], parsed.line_separator, config) output.append(parsed.line_separator.join(lines)) else: output.append( @@ -719,13 +725,15 @@ def _with_straight_imports( if inline_comments: combined_inline_comments = " ".join(c for c in inline_comments if c) if combined_inline_comments: - output.append( + line_content = ( f"{import_type} {combined_straight_imports} # {combined_inline_comments}" ) else: - output.append(f"{import_type} {combined_straight_imports} #") + line_content = f"{import_type} {combined_straight_imports} #" else: - output.append(f"{import_type} {combined_straight_imports}") + line_content = f"{import_type} {combined_straight_imports}" + + output.append(wrap.line(line_content, parsed.line_separator, config)) return output @@ -747,15 +755,16 @@ def _with_straight_imports( comments_above = parsed.categorized_comments["above"]["straight"].pop(module, None) if comments_above: output.extend(comments_above) - output.extend( - with_comments( + for idef, imodule in import_definition: + line_content = with_comments( parsed.categorized_comments["straight"].get(imodule), idef, removed=config.ignore_comments, comment_prefix=config.comment_prefix, ) - for idef, imodule in import_definition - ) + if config.multi_line_output == wrap.Modes.NOQA: # type: ignore[attr-defined] + line_content = wrap.line(line_content, parsed.line_separator, config) + output.append(line_content) return output diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index f843ff84..e47ab93a 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -2117,3 +2117,90 @@ 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_noqa_added_to_long_force_single_line_as_import_with_comment_issue_2093(): + """A long ``as`` import with inline comment must get ``# NOQA`` in NOQA mode. + + With ``force_single_line`` an aliased import that carries an inline comment + and overflows the line length must still receive ``# NOQA``. Previously + ``with_comments()`` wrapped ``wrap.line()``, so the length check ran on the + import without the comment and ``# NOQA`` was never added. + """ + to_sort = ( + "from my_package.my_module import super_long_file_name as super_long_alias" + " # type: ignore\n" + ) + + first_pass = isort.code(to_sort, multi_line_output=7, force_single_line=True, line_length=40) + assert first_pass == ( + "from my_package.my_module import super_long_file_name as super_long_alias" + " # type: ignore # NOQA\n" + ) + assert first_pass.count("NOQA") == 1 + + second_pass = isort.code( + first_pass, multi_line_output=7, force_single_line=True, line_length=40 + ) + assert second_pass == first_pass + + +def test_noqa_added_to_long_as_import_with_opening_comment_issue_2093(): + """A long ``as`` import with opening-line comment must get ``# NOQA`` in NOQA mode. + + When ``use_parentheses`` is enabled, opening-line comments are kept on the + ``from X import (`` line. The ``wrap.line()`` call ran before the comment was + attached, so it saw a short import and never added ``# NOQA``. The fix re-runs + ``wrap.line()`` in NOQA mode after the comment is attached. + """ + to_sort = ( + "from my_package.my_module import (\n" + " super_long_file_name as super_long_alias # type: ignore\n" + ")\n" + ) + + first_pass = isort.code(to_sort, multi_line_output=7, line_length=40) + assert "# NOQA" in first_pass + assert first_pass.count("NOQA") == 1 + + second_pass = isort.code(first_pass, multi_line_output=7, line_length=40) + assert second_pass == first_pass + + +def test_noqa_added_to_long_combined_straight_imports_issue_2093(): + """Long combined straight imports must get ``# NOQA`` in NOQA mode. + + With ``combine_straight_imports`` enabled, multiple ``import`` statements + are merged into a single ``import a, b, c, ...`` line. Previously this line + was appended directly without going through ``wrap.line()``, so ``# NOQA`` + was never added even when it exceeded the line length. + """ + to_sort = "import a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p\n" + + first_pass = isort.code( + to_sort, multi_line_output=7, combine_straight_imports=True, line_length=40 + ) + assert first_pass == ("import a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p # NOQA\n") + assert first_pass.count("NOQA") == 1 + + second_pass = isort.code( + first_pass, multi_line_output=7, combine_straight_imports=True, line_length=40 + ) + assert second_pass == first_pass + + +def test_noqa_added_to_long_straight_import_issue_2093(): + """Long straight imports must get ``# NOQA`` in NOQA mode. + + Straight imports (``import x`` or ``import x as y``) that exceed the line + length were emitted directly without going through ``wrap.line()``, so + ``# NOQA`` was never added. The fix wraps each import through ``wrap.line()``. + """ + to_sort = "import aaaa_long_module_name as bbbb_long_alias_name\n" + + first_pass = isort.code(to_sort, multi_line_output=7, line_length=40) + assert first_pass == "import aaaa_long_module_name as bbbb_long_alias_name # NOQA\n" + assert first_pass.count("NOQA") == 1 + + second_pass = isort.code(first_pass, multi_line_output=7, line_length=40) + assert second_pass == first_pass