Skip to content

Commit fd8bd07

Browse files
authored
Merge pull request #2574 from apoorvdarshan/fix-isort-skip-with-future-import-2092
Honor `# isort: skip` when a __future__ import is present (#2092)
2 parents 6dcdb84 + 793a91e commit fd8bd07

2 files changed

Lines changed: 43 additions & 1 deletion

File tree

isort/core.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@
3030
"# isort: assignments",
3131
)
3232
LITERAL_TYPE_MAPPING = {"(": "tuple", "[": "list", "{": "set"}
33+
SKIP_IMPORT_COMMENTS = ("isort:skip", "isort: skip")
34+
35+
36+
def _has_skip_comment(import_statement: str) -> bool:
37+
"""Return whether an import statement carries a per-line ``isort: skip`` directive."""
38+
return any(comment in import_statement for comment in SKIP_IMPORT_COMMENTS)
3339

3440

3541
# Ignore DeepSource cyclomatic complexity check for this function.
@@ -325,10 +331,17 @@ def process(
325331
stripped_line = line.strip().split("#")[0]
326332
import_statement += line
327333

334+
# The second clause keeps a per-line ``isort: skip`` import exactly
335+
# where it is: when earlier imports have already been collected into
336+
# the current section, the skipped statement is treated as a section
337+
# boundary so those imports can't be sorted above it. Without it, a
338+
# preceding import (most commonly a ``__future__`` import, which is
339+
# always floated to the top) makes isort splice the sorted block
340+
# ahead of the skipped line and relocate it below the block. See #2092.
328341
if (
329342
import_statement.lstrip().startswith("from")
330343
and "import" not in import_statement
331-
):
344+
) or (contains_imports and _has_skip_comment(import_statement)):
332345
line = import_statement
333346
not_imports = True
334347
else:

tests/unit/test_regressions.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2260,3 +2260,32 @@ def test_noqa_added_to_long_combined_straight_imports_with_bare_comment_issue_20
22602260
to_sort, multi_line_output=7, combine_straight_imports=True, line_length=40
22612261
)
22622262
assert first_pass == ("import a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p # # NOQA\n")
2263+
2264+
2265+
def test_isort_skip_is_honored_with_future_import_issue_2092():
2266+
"""A per-line ``isort: skip`` must be honored even when a ``__future__`` import is present.
2267+
2268+
``__future__`` imports are always floated to the very top, which used to make isort splice
2269+
the sorted import block ahead of a following ``# isort: skip`` line and relocate the skipped
2270+
import below the block - silently violating the skip directive. The skipped import must stay
2271+
exactly where it is, and the result must be stable across re-runs. See issue #2092.
2272+
"""
2273+
to_sort = (
2274+
"from __future__ import annotations\n"
2275+
"\n"
2276+
"from foo import bar # isort: skip\n"
2277+
"from bar import baz\n"
2278+
)
2279+
2280+
first_pass = isort.code(to_sort)
2281+
assert first_pass == to_sort
2282+
assert isort.check_code(to_sort, show_diff=True)
2283+
2284+
# An interleaved skip (between two regular imports) must keep its position rather than
2285+
# being sorted to the bottom of the block, and must be idempotent.
2286+
interleaved = "import aaa\nfrom foo import bar # isort: skip\nimport ccc\n"
2287+
sorted_interleaved = isort.code(interleaved)
2288+
lines = sorted_interleaved.splitlines()
2289+
skip_index = next(i for i, line in enumerate(lines) if "# isort: skip" in line)
2290+
assert lines.index("import ccc") > skip_index # skip not relocated below the block
2291+
assert isort.code(sorted_interleaved) == sorted_interleaved

0 commit comments

Comments
 (0)