From 47cf7ae923320264ee4206d3d86a0caf2cf1518b Mon Sep 17 00:00:00 2001 From: linhongkuan Date: Thu, 25 Jun 2026 10:34:03 +0800 Subject: [PATCH] Preserve form feed blank lines --- CHANGELOG.md | 2 ++ isort/output.py | 10 +++++++--- isort/parse.py | 7 ++++--- tests/unit/test_regressions.py | 6 ++++++ 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d0ad2a7..bac553d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ Find out more about isort's release policy [here](docs/major_releases/release_po ### Unreleased + - Preserve form feed characters when they appear on otherwise blank lines (#2562) + ### 8.0.0 February 19 2026 - Removed `--old-finders` and `--magic-placement` flags and `old_finders` configuration option. The legacy finder logic that relied on environment introspection has been removed (#2445) @joao-faria-dev diff --git a/isort/output.py b/isort/output.py index ada89f75..8f5b5304 100644 --- a/isort/output.py +++ b/isort/output.py @@ -132,9 +132,9 @@ def sorted_imports( if output: imports_tail = output_at + len(output) - while [ - character.strip() for character in formatted_output[imports_tail : imports_tail + 1] - ] == [""]: + while formatted_output[imports_tail : imports_tail + 1] and _is_removable_blank_line( + formatted_output[imports_tail] + ): formatted_output.pop(imports_tail) if config.lines_before_imports != -1: @@ -191,6 +191,10 @@ def sorted_imports( return _output_as_string(formatted_output, parsed.line_separator) +def _is_removable_blank_line(line: str) -> bool: + return "\f" not in line and line.strip() == "" + + # Ignore DeepSource cyclomatic complexity check for this function. # skipcq: PY-R1000 def _build_import_group( diff --git a/isort/parse.py b/isort/parse.py index 03678397..35919168 100644 --- a/isort/parse.py +++ b/isort/parse.py @@ -1,5 +1,6 @@ """Defines parsing functions used by isort for parsing import definitions""" +import re from collections import OrderedDict, defaultdict from functools import partial from itertools import chain @@ -19,6 +20,8 @@ from .exceptions import MissingSection from .settings import DEFAULT_CONFIG, Config +NEWLINE_RE = re.compile(r"\r\n|\r|\n") + if TYPE_CHECKING: CommentsAboveDict = TypedDict( "CommentsAboveDict", {"straight": dict[str, list[str]], "from": dict[str, list[str]]} @@ -77,9 +80,7 @@ class ParsedContent(NamedTuple): def file_contents(contents: str, config: Config = DEFAULT_CONFIG) -> ParsedContent: """Parses a python file taking out and categorizing imports.""" line_separator: str = config.line_ending or _infer_line_separator(contents) - in_lines = contents.splitlines() - if contents and contents[-1] in ("\n", "\r"): - in_lines.append("") + in_lines = NEWLINE_RE.split(contents) if contents else [] out_lines = [] original_line_count = len(in_lines) diff --git a/tests/unit/test_regressions.py b/tests/unit/test_regressions.py index f843ff84..3cfea6c0 100644 --- a/tests/unit/test_regressions.py +++ b/tests/unit/test_regressions.py @@ -99,6 +99,12 @@ def test_blank_lined_removed_issue_1283(): assert isort.code(test_input) == test_input +def test_form_feed_blank_line_not_removed_issue_2562(): + """Ensure isort preserves form feed as a valid blank line.""" + test_input = 'import os\nimport sys\n\n\f\nprint("Hello, world!")\n' + assert isort.code(test_input) == test_input + + def test_extra_blank_line_added_nested_imports_issue_1290(): """Ensure isort doesn't add unnecessary blank lines above nested imports. See: https://github.com/pycqa/isort/issues/1290