Stabilize pragma comment width handling (#28795)

Summary
--

I wasn't really planning to do a full stabilization pass, but I quickly skimmed through `preview.rs`
and remembered that the isort pragma change was kind a bug fix but technically breaking and thought
we might want to stabilize it.

This PR stabilizes the isort pragma comment width handling introduced in #27313, along with the
related pragma handling for `E501` and `W505` from #24071. There was a comment about also
stabilizing the related formatter preview feature on the function from #24071, so that is stabilized
here too.

I'm fine holding off on this, especially if the formatter change is a better fit for the 2027 style
guide. Like I said above, I mostly wanted to stabilize the isort change, but that snowballed a bit
since the helpers were shared.

Test Plan
--

Updated existing snapshots
This commit is contained in:
Brent Westbrook committed 2026-10-09 12:26:57 -04:00
1 parent c94a19d3bc
commit 3f5a91cecf
25 files changed
+80 -318

No files matched your search

@@ -9,7 +9,7 @@ from aaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaa import ( # explain
# non-pragma prefix is 86 columns.
from bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb import x # explain # noqa: TID251
# Separate comments with the pragma first: when isort collapses this import in preview,
# Separate comments with the pragma first: when isort collapses this import,
# the merged comment token is pragma-prefixed, so E501 strips it entirely and the
# collapsed 104-column line is fine.
from ccccccccccccccccccccccccccccccc.ccccccccccccccccccccccccccccccc import ( # noqa: TID251
@@ -1,5 +1,5 @@
# The next import fits on one line once the trailing pragma is excluded from the width
# (the `# keep this` prefix still counts); in preview it should not be wrapped.
# (the `# keep this` prefix still counts); it should not be wrapped.
from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa import x # keep this # noqa: TID251
# The next import exceeds the line length even without the trailing pragma
# (code plus the `# keep this` prefix is 89 columns); it must always be wrapped.
@@ -1,5 +1,5 @@
# The next import fits on one line once the pragma comment is excluded from the width;
# in preview it should not be wrapped (the `# noqa` must stay effective).
# it should not be wrapped (the `# noqa` must stay effective).
from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa import x # noqa: TID251
# The next import exceeds the line length even without the pragma comment;
# it must still be wrapped.
@@ -8,6 +8,6 @@ from bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
def f():
# The next import fits on one line once the pragma comment is excluded from the
# width, so in preview it should not be wrapped.
# width, so it should not be wrapped.
from cccccccccccccccccccccccccccccc.ccccccccccccccccccccccccccccccccccccc import bar # noqa: PLC0415
bar()
+5 -40
View File
@@ -9,35 +9,7 @@ use unicode_width::UnicodeWidthChar;
use ruff_cache::{CacheKey, CacheKeyHasher};
use ruff_macros::CacheKey;
use ruff_python_trivia::{find_trailing_pragma_offset, is_pragma_comment, tab_offset};
use crate::preview::{
is_pragma_excluded_from_import_width_enabled, is_trailing_pragma_in_line_length_enabled,
};
use crate::settings::types::PreviewMode;
/// Returns the offset within `comment` at which the pragma comment excluded from line-length
/// measurement begins, or `None` if the comment contains no such pragma.
///
/// This is the shared policy for how pragma comments (e.g., `# noqa: F401` or `# type: ignore`)
/// are excluded when measuring line width, used by `line-too-long` (E501) and
/// `doc-line-too-long` (W505), and, in preview mode, by isort's (I001) decision of whether an
/// import fits on one line (see [`LineWidthBuilder::add_comment`]). The formatter applies the
/// equivalent policy when measuring comment widths.
///
/// In stable mode, only comments that are pragmas in their entirety are excluded (the returned
/// offset is `0`). In preview mode, a trailing pragma within a mixed comment (e.g.,
/// `# explanation # noqa: F401`) is also excluded, in which case the offset points at the `#`
/// that begins the pragma.
pub(crate) fn pragma_offset_for_line_length(comment: &str, preview: PreviewMode) -> Option<usize> {
if is_trailing_pragma_in_line_length_enabled(preview) {
find_trailing_pragma_offset(comment)
} else if is_pragma_comment(comment) {
Some(0)
} else {
None
}
}
use ruff_python_trivia::{find_trailing_pragma_offset, tab_offset};
/// The length of a line of text that is considered too long.
///
@@ -267,8 +239,8 @@ impl LineWidthBuilder {
}
/// Adds the width of a trailing comment, including the standard two-space separator that
/// precedes it. In preview mode, any pragma comment is excluded per
/// [`pragma_offset_for_line_length`].
/// precedes it. Pragma suffixes are excluded per [`find_trailing_pragma_offset`], while
/// any preceding non-pragma comment text still contributes to the width.
///
/// Pragma comments are excluded so that adding one to a line never affects whether the line
/// is considered to fit, consistent with how `line-too-long` (E501) measures lines. For
@@ -281,16 +253,9 @@ impl LineWidthBuilder {
/// member, # noqa: PLC0415
/// )
/// ```
///
/// Unlike E501, which has always stripped whole-pragma comments on stable, the exclusion
/// changes how imports are formatted, so it is preview-gated in its entirety: on stable, the
/// full comment width is counted.
#[must_use]
pub(crate) fn add_comment(self, comment: &str, preview: PreviewMode) -> Self {
if !is_pragma_excluded_from_import_width_enabled(preview) {
return self.add_width(2).add_str(comment);
}
let counted = match pragma_offset_for_line_length(comment, preview) {
pub(crate) fn add_comment(self, comment: &str) -> Self {
let counted = match find_trailing_pragma_offset(comment) {
Some(offset) => comment[..offset].trim_end(),
None => comment,
};
-11
View File
@@ -329,12 +329,6 @@ pub(crate) const fn is_up006_future_annotations_fix_enabled(settings: &LinterSet
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/24071
// Make sure to stabilize the corresponding formatter preview behavior when stabilizing this preview style.
pub(crate) const fn is_trailing_pragma_in_line_length_enabled(preview: PreviewMode) -> bool {
preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/24371
pub(crate) const fn is_collapsible_if_fix_safe_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
@@ -362,11 +356,6 @@ pub(crate) const fn is_b005_precise_diagnostic_enabled(settings: &LinterSettings
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/27313
pub(crate) const fn is_pragma_excluded_from_import_width_enabled(preview: PreviewMode) -> bool {
preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/27666
pub(crate) const fn is_rule_categories_enabled(preview: PreviewMode) -> bool {
preview.is_enabled()
+5 -10
View File
@@ -1,7 +1,6 @@
use ruff_python_codegen::Stylist;
use crate::line_width::{LineLength, LineWidthBuilder};
use crate::settings::types::PreviewMode;
use super::types::{AliasData, ImportCommentSet, ImportFromCommentSet, ImportFromData, Importable};
@@ -55,7 +54,6 @@ pub(crate) fn format_import_from(
force_wrap_aliases: bool,
is_first: bool,
trailing_comma: bool,
preview: PreviewMode,
) -> String {
if aliases.len() == 1
&& aliases
@@ -69,7 +67,6 @@ pub(crate) fn format_import_from(
is_first,
stylist,
indentation_width,
preview,
);
return single_line;
}
@@ -98,7 +95,6 @@ pub(crate) fn format_import_from(
is_first,
stylist,
indentation_width,
preview,
);
if import_width <= line_length || aliases.iter().any(|(alias, _)| alias.name == "*") {
return single_line;
@@ -118,7 +114,6 @@ fn format_single_line(
is_first: bool,
stylist: &Stylist,
indentation_width: LineWidthBuilder,
preview: PreviewMode,
) -> (String, LineWidthBuilder) {
let mut output = String::with_capacity(CAPACITY);
let mut line_width = indentation_width;
@@ -161,7 +156,7 @@ fn format_single_line(
output.push(' ');
output.push(' ');
output.push_str(comment);
line_width = line_width.add_comment(comment, preview);
line_width = line_width.add_comment(comment);
}
for (_, comments) in aliases {
@@ -169,21 +164,21 @@ fn format_single_line(
output.push(' ');
output.push(' ');
output.push_str(comment);
line_width = line_width.add_comment(comment, preview);
line_width = line_width.add_comment(comment);
}
for comment in &comments.inline {
output.push(' ');
output.push(' ');
output.push_str(comment);
line_width = line_width.add_comment(comment, preview);
line_width = line_width.add_comment(comment);
}
for comment in &comments.trailing {
output.push(' ');
output.push(' ');
output.push_str(comment);
line_width = line_width.add_comment(comment, preview);
line_width = line_width.add_comment(comment);
}
}
@@ -191,7 +186,7 @@ fn format_single_line(
output.push(' ');
output.push(' ');
output.push_str(comment);
line_width = line_width.add_comment(comment, preview);
line_width = line_width.add_comment(comment);
}
output.push_str(&stylist.line_ending());
+5 -29
View File
@@ -20,7 +20,6 @@ use types::{AliasData, ImportBlock, TrailingComma};
use crate::Locator;
use crate::line_width::{LineLength, LineWidthBuilder};
use crate::package::PackageRoot;
use crate::settings::types::PreviewMode;
use ruff_python_ast::PythonVersion;
mod annotate;
@@ -78,7 +77,6 @@ pub(crate) fn format_imports(
source_type: PySourceType,
target_version: PythonVersion,
settings: &Settings,
preview: PreviewMode,
tokens: &Tokens,
) -> String {
let trailer = &block.trailer;
@@ -106,7 +104,6 @@ pub(crate) fn format_imports(
package,
target_version,
settings,
preview,
);
if !block_output.is_empty() && !output.is_empty() {
@@ -163,7 +160,6 @@ fn format_import_block(
package: Option<PackageRoot<'_>>,
target_version: PythonVersion,
settings: &Settings,
preview: PreviewMode,
) -> String {
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum LineInsertion {
@@ -293,7 +289,6 @@ fn format_import_block(
is_first_statement,
settings.split_on_trailing_comma
&& matches!(trailing_comma, TrailingComma::Present),
preview,
));
if settings.from_first {
@@ -323,7 +318,7 @@ mod tests {
use crate::registry::Rule;
use crate::rules::isort::categorize::{ImportSection, KnownModules};
use crate::settings::LinterSettings;
use crate::settings::types::{IdentifierPattern, PreviewMode};
use crate::settings::types::IdentifierPattern;
use crate::source_kind::SourceKind;
use crate::test::{test_contents, test_path, test_resource_path};
@@ -399,38 +394,19 @@ mod tests {
Ok(())
}
#[test_case(Path::new("fit_line_length_mixed_pragma.py"))]
#[test_case(Path::new("fit_line_length_pragma.py"))]
fn preview(path: &Path) -> Result<()> {
let snapshot = format!("preview__{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("isort").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Enabled,
src: vec![test_resource_path("fixtures/isort")],
..LinterSettings::for_rule(Rule::UnsortedImports)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
/// Fixing I001 must never leave behind a line that E501 then flags.
///
/// isort excludes pragma comments (e.g., `# noqa: TID251`) from its width computation
/// per comment, while E501 measures the emitted line's single comment token as a whole.
/// When isort merges separate comments onto one line (e.g., a statement-level `# explain`
/// and an alias-level `# noqa`), the two computations could disagree; this test verifies
/// that the fix nonetheless converges to output that E501 accepts, in both stable and
/// preview modes.
#[test_case(PreviewMode::Disabled, "stable")]
#[test_case(PreviewMode::Enabled, "preview")]
fn no_line_too_long_after_fix(preview: PreviewMode, label: &str) -> Result<()> {
/// that the fix nonetheless converges to output that E501 accepts.
#[test]
fn no_line_too_long_after_fix() -> Result<()> {
let path = test_resource_path("fixtures").join("isort/fit_line_length_merged_pragma.py");
let source_type = SourceType::Python(PySourceType::from(&path));
let source_kind = SourceKind::from_path(&path, source_type)?.expect("valid source");
let settings = LinterSettings {
preview,
src: vec![test_resource_path("fixtures/isort")],
..LinterSettings::for_rules([Rule::UnsortedImports, Rule::LineTooLong])
};
@@ -438,7 +414,7 @@ mod tests {
// `test_contents` applies fixes to convergence (and panics if they fail to converge).
let (_, transformed) = test_contents(&source_kind, &path, &settings);
insta::assert_snapshot!(
format!("fit_line_length_merged_pragma_fixed_{label}"),
"fit_line_length_merged_pragma_fixed",
transformed.source_code()
);
@@ -170,7 +170,6 @@ pub(crate) fn organize_imports(
source_type,
target_version,
&settings.isort,
settings.preview,
tokens,
);
@@ -11,7 +11,7 @@ from aaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaa import x # explain # noq
# non-pragma prefix is 86 columns.
from bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb import x # explain # noqa: TID251
# Separate comments with the pragma first: when isort collapses this import in preview,
# Separate comments with the pragma first: when isort collapses this import,
# the merged comment token is pragma-prefixed, so E501 strips it entirely and the
# collapsed 104-column line is fine.
from ccccccccccccccccccccccccccccccc.ccccccccccccccccccccccccccccccc import x # noqa: TID251 # explain
@@ -1,23 +0,0 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
expression: transformed.source_code()
---
# Separate statement-level and alias-level comments that isort merges onto one line
# when collapsing. The merged comment token is 89 columns with the `# explain` prefix
# counted, so the import must not end up on an overlong single line.
from aaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaa import ( # explain
x, # noqa: TID251
)
# A single mixed comment on an already-collapsed 102-column line. The code plus the
# non-pragma prefix is 86 columns.
from bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb import (
x, # explain # noqa: TID251
)
# Separate comments with the pragma first: when isort collapses this import in preview,
# the merged comment token is pragma-prefixed, so E501 strips it entirely and the
# collapsed 104-column line is fine.
from ccccccccccccccccccccccccccccccc.ccccccccccccccccccccccccccccccc import ( # noqa: TID251
x, # explain
)
@@ -5,24 +5,20 @@ I001 [*] Import block is un-sorted or un-formatted
--> fit_line_length_mixed_pragma.py:3:1
|
1 | # The next import fits on one line once the trailing pragma is excluded from the width
2 | # (the `# keep this` prefix still counts); in preview it should not be wrapped.
2 | # (the `# keep this` prefix still counts); it should not be wrapped.
3 | / from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa import x # keep this # noqa: TID251
4 | | # The next import exceeds the line length even without the trailing pragma
5 | | # (code plus the `# keep this` prefix is 89 columns); it must always be wrapped.
6 | | from bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb import x # keep this # noqa: TID251
| |____________________________________________________________________________^
help: Organize imports
|
2 | # (the `# keep this` prefix still counts); in preview it should not be wrapped.
- from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa import x # keep this # noqa: TID251
3 + from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa import (
4 + x, # keep this # noqa: TID251
5 + )
6 +
7 | # The next import exceeds the line length even without the trailing pragma
8 | # (code plus the `# keep this` prefix is 89 columns); it must always be wrapped.
- from bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb import x # keep this # noqa: TID251
9 + from bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb import (
10 + x, # keep this # noqa: TID251
11 + )
|
|
3 | from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa import x # keep this # noqa: TID251
4 +
5 | # The next import exceeds the line length even without the trailing pragma
6 | # (code plus the `# keep this` prefix is 89 columns); it must always be wrapped.
- from bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb import x # keep this # noqa: TID251
7 + from bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb import (
8 + x, # keep this # noqa: TID251
9 + )
|
@@ -5,7 +5,7 @@ I001 [*] Import block is un-sorted or un-formatted
--> fit_line_length_pragma.py:3:1
|
1 | # The next import fits on one line once the pragma comment is excluded from the width;
2 | # in preview it should not be wrapped (the `# noqa` must stay effective).
2 | # it should not be wrapped (the `# noqa` must stay effective).
3 | / from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa import x # noqa: TID251
4 | | # The next import exceeds the line length even without the pragma comment;
5 | | # it must still be wrapped.
@@ -13,36 +13,13 @@ I001 [*] Import block is un-sorted or un-formatted
| |_________________________________________________________________________________________^
help: Organize imports
|
2 | # in preview it should not be wrapped (the `# noqa` must stay effective).
- from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa import x # noqa: TID251
3 + from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa import (
4 + x, # noqa: TID251
5 + )
6 +
7 | # The next import exceeds the line length even without the pragma comment;
8 | # it must still be wrapped.
3 | from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa import x # noqa: TID251
4 +
5 | # The next import exceeds the line length even without the pragma comment;
6 | # it must still be wrapped.
- from bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb import x # noqa: TID251
9 + from bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb import (
10 + x, # noqa: TID251
11 + )
12 |
|
I001 [*] Import block is un-sorted or un-formatted
--> fit_line_length_pragma.py:12:5
|
10 | # The next import fits on one line once the pragma comment is excluded from the
11 | # width, so in preview it should not be wrapped.
12 | from cccccccccccccccccccccccccccccc.ccccccccccccccccccccccccccccccccccccc import bar # noqa: PLC0415
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
13 | bar()
|
help: Organize imports
|
11 | # width, so in preview it should not be wrapped.
- from cccccccccccccccccccccccccccccc.ccccccccccccccccccccccccccccccccccccc import bar # noqa: PLC0415
12 + from cccccccccccccccccccccccccccccc.ccccccccccccccccccccccccccccccccccccc import (
13 + bar, # noqa: PLC0415
14 + )
15 | bar()
7 + from bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb import (
8 + x, # noqa: TID251
9 + )
10 |
|
@@ -17,21 +17,15 @@ I001 [*] Import block is un-sorted or un-formatted
11 | | from d.prometheus.metrics import TERMINAL_CURRENTLY_RUNNING_TOTAL, OTHER_RUNNING_TOTAL # type:ignore[attr-defined]
| |______________________________________________________________________________________^
help: Organize imports
|
3 | )
-
4 | from b.prometheus.metrics import (
5 | TERMINAL_CURRENTLY_RUNNING_TOTAL, # type:ignore[attr-defined]
6 | )
-
- from c.prometheus.metrics import TERMINAL_CURRENTLY_RUNNING_TOTAL # type:ignore[attr-defined]
-
- from d.prometheus.metrics import TERMINAL_CURRENTLY_RUNNING_TOTAL, OTHER_RUNNING_TOTAL # type:ignore[attr-defined]
7 + from c.prometheus.metrics import (
8 + TERMINAL_CURRENTLY_RUNNING_TOTAL, # type:ignore[attr-defined]
9 + )
10 + from d.prometheus.metrics import ( # type:ignore[attr-defined]
11 + OTHER_RUNNING_TOTAL,
12 + TERMINAL_CURRENTLY_RUNNING_TOTAL,
13 + )
|
|
3 | )
-
4 | from b.prometheus.metrics import (
5 | TERMINAL_CURRENTLY_RUNNING_TOTAL, # type:ignore[attr-defined]
6 | )
-
7 | from c.prometheus.metrics import TERMINAL_CURRENTLY_RUNNING_TOTAL # type:ignore[attr-defined]
-
- from d.prometheus.metrics import TERMINAL_CURRENTLY_RUNNING_TOTAL, OTHER_RUNNING_TOTAL # type:ignore[attr-defined]
8 + from d.prometheus.metrics import OTHER_RUNNING_TOTAL, TERMINAL_CURRENTLY_RUNNING_TOTAL # type:ignore[attr-defined]
|
@@ -1,24 +0,0 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
I001 [*] Import block is un-sorted or un-formatted
--> fit_line_length_mixed_pragma.py:3:1
|
1 | # The next import fits on one line once the trailing pragma is excluded from the width
2 | # (the `# keep this` prefix still counts); in preview it should not be wrapped.
3 | / from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa import x # keep this # noqa: TID251
4 | | # The next import exceeds the line length even without the trailing pragma
5 | | # (code plus the `# keep this` prefix is 89 columns); it must always be wrapped.
6 | | from bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb import x # keep this # noqa: TID251
| |____________________________________________________________________________^
help: Organize imports
|
3 | from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa import x # keep this # noqa: TID251
4 +
5 | # The next import exceeds the line length even without the trailing pragma
6 | # (code plus the `# keep this` prefix is 89 columns); it must always be wrapped.
- from bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb import x # keep this # noqa: TID251
7 + from bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb import (
8 + x, # keep this # noqa: TID251
9 + )
|
@@ -1,25 +0,0 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
I001 [*] Import block is un-sorted or un-formatted
--> fit_line_length_pragma.py:3:1
|
1 | # The next import fits on one line once the pragma comment is excluded from the width;
2 | # in preview it should not be wrapped (the `# noqa` must stay effective).
3 | / from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa import x # noqa: TID251
4 | | # The next import exceeds the line length even without the pragma comment;
5 | | # it must still be wrapped.
6 | | from bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb import x # noqa: TID251
| |_________________________________________________________________________________________^
help: Organize imports
|
3 | from aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa import x # noqa: TID251
4 +
5 | # The next import exceeds the line length even without the pragma comment;
6 | # it must still be wrapped.
- from bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb import x # noqa: TID251
7 + from bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb import (
8 + x, # noqa: TID251
9 + )
10 |
|
@@ -38,6 +38,7 @@ mod tests {
#[test_case(Rule::LineTooLong, Path::new("E501.py"))]
#[test_case(Rule::LineTooLong, Path::new("E501_3.py"))]
#[test_case(Rule::LineTooLong, Path::new("E501_4.py"))]
#[test_case(Rule::LineTooLong, Path::new("E501_5.py"))]
#[test_case(Rule::MixedSpacesAndTabs, Path::new("E101.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E40.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_0.py"))]
@@ -75,7 +76,6 @@ mod tests {
Ok(())
}
#[test_case(Rule::LineTooLong, Path::new("E501_5.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E40.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_0.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_1.py"))]
@@ -1,11 +1,10 @@
use std::ops::Deref;
use ruff_python_trivia::CommentRanges;
use ruff_python_trivia::{CommentRanges, find_trailing_pragma_offset};
use ruff_source_file::Line;
use ruff_text_size::{TextLen, TextRange};
use crate::line_width::{IndentWidth, LineLength, LineWidthBuilder, pragma_offset_for_line_length};
use crate::settings::types::PreviewMode;
use crate::line_width::{IndentWidth, LineLength, LineWidthBuilder};
#[derive(Debug)]
pub(super) struct Overlong {
@@ -22,7 +21,6 @@ impl Overlong {
limit: LineLength,
task_tags: &[String],
tab_size: IndentWidth,
preview: PreviewMode,
) -> Option<Self> {
// The maximum width of the line is the number of bytes multiplied by the tab size (the
// worst-case scenario is that the line is all tabs). If the maximum width is less than the
@@ -39,7 +37,7 @@ impl Overlong {
}
// Strip trailing comments and re-measure the line, if needed.
let line = StrippedLine::from_line(line, comment_ranges, task_tags, preview);
let line = StrippedLine::from_line(line, comment_ranges, task_tags);
let width = match &line {
StrippedLine::WithoutPragma(line) => {
let width = measure(line.as_str(), tab_size);
@@ -118,12 +116,7 @@ enum StrippedLine<'a> {
impl<'a> StrippedLine<'a> {
/// Strip trailing comments from a [`Line`], if the line ends with a pragma comment (like
/// `# type: ignore`) or, if necessary, a task comment (like `# TODO`).
fn from_line(
line: &'a Line<'a>,
comment_ranges: &CommentRanges,
task_tags: &[String],
preview: PreviewMode,
) -> Self {
fn from_line(line: &'a Line<'a>, comment_ranges: &CommentRanges, task_tags: &[String]) -> Self {
let [comment_range] = comment_ranges.comments_in_range(line.range()) else {
return Self::Unchanged(line);
};
@@ -132,8 +125,8 @@ impl<'a> StrippedLine<'a> {
let comment_range = comment_range - line.start();
let comment = &line.as_str()[comment_range];
// Ex) `# type: ignore` or (in preview) `# some comment # noqa: F401`
if let Some(offset) = pragma_offset_for_line_length(comment, preview) {
// Ex) `# type: ignore` or `# some comment # noqa: F401`
if let Some(offset) = find_trailing_pragma_offset(comment) {
// Strip the pragma from the line, preserving any preceding non-pragma comment text.
let pragma_start = usize::from(comment_range.start()) + offset;
let prefix = line[..pragma_start].trim_end();
@@ -105,7 +105,6 @@ pub(crate) fn doc_line_too_long(
&[]
},
settings.tab_size,
settings.preview,
) {
context.report_diagnostic(
DocLineTooLong(overlong.width(), limit.value() as usize),
@@ -101,7 +101,6 @@ pub(crate) fn line_too_long(
&[]
},
settings.tab_size,
settings.preview,
) {
context.report_diagnostic(
LineTooLong(overlong.width(), limit.value() as usize),
@@ -1,5 +1,5 @@
# Trailing pragma after another comment - the pragma portion should not count
# toward reserved width in preview mode.
# toward reserved width.
# The expression is long enough that the formatter would break it if the full
# comment width were reserved, but short enough to fit if only the non-pragma
@@ -9,9 +9,9 @@ i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # comment
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # comment # pyright: ignore
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) ## noqa: F401
# Plain pragma (no nested comment) - should behave the same in stable and preview
# Plain pragma (no nested comment) - should not reserve any width
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: F401
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: ignore
# Not a pragma - should reserve full width in both modes
# Not a pragma - should reserve full width
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # comment # not a pragma
@@ -3,15 +3,14 @@ use std::borrow::Cow;
use ruff_formatter::{FormatError, FormatOptions, SourceCode, format_args, write};
use ruff_python_ast::{AnyNodeRef, NodeKind, PySourceType};
use ruff_python_trivia::{
CommentLinePosition, find_trailing_pragma_offset, is_pragma_comment, lines_after,
lines_after_ignoring_trivia, lines_before,
CommentLinePosition, find_trailing_pragma_offset, lines_after, lines_after_ignoring_trivia,
lines_before,
};
use ruff_text_size::{Ranged, TextLen, TextRange};
use crate::comments::SourceComment;
use crate::context::NodeLevel;
use crate::prelude::*;
use crate::preview::is_trailing_pragma_in_comment_width_enabled;
use crate::statement::suite::should_insert_blank_line_after_class_in_stub_file;
/// Formats the leading comments of a node.
@@ -378,18 +377,11 @@ impl Format<PyFormatContext<'_>> for FormatTrailingEndOfLineComment<'_> {
let normalized_comment = normalize_comment(self.comment, source)?;
// Don't reserve width for pragma comments. In preview, comments
// containing a trailing pragma (e.g., `# comment # noqa: F401`) only
// reserve width for the non-pragma prefix.
let non_pragma_comment_part = if is_trailing_pragma_in_comment_width_enabled(f.context()) {
match find_trailing_pragma_offset(&normalized_comment) {
Some(offset) => normalized_comment[..offset].trim_end(),
None => &normalized_comment,
}
} else if is_pragma_comment(&normalized_comment) {
""
} else {
&normalized_comment
// Don't reserve width for pragma comments. Comments containing a trailing pragma
// (e.g., `# comment # noqa: F401`) only reserve width for the non-pragma prefix.
let non_pragma_comment_part = match find_trailing_pragma_offset(&normalized_comment) {
Some(offset) => normalized_comment[..offset].trim_end(),
None => &normalized_comment,
};
let reserved_width = if non_pragma_comment_part.is_empty() {
@@ -20,13 +20,3 @@ pub(crate) const fn is_hug_parens_with_braces_and_square_brackets_enabled(
pub(crate) const fn is_fluent_layout_split_first_call_enabled(context: &PyFormatContext) -> bool {
context.is_preview()
}
/// Returns `true` if the
/// [trailing pragma width handling in comments](https://github.com/astral-sh/ruff/pull/24071)
/// is enabled.
/// When enabled, comments like `# text # noqa: F401` only reserve width for
/// the non-pragma prefix (`# text`), not the trailing pragma.
/// Make sure to stabilize the corresponding linter preview behavior when stabilizing this preview style.
pub(crate) const fn is_trailing_pragma_in_comment_width_enabled(context: &PyFormatContext) -> bool {
context.is_preview()
}
@@ -5,7 +5,7 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/trailing_p
## Input
```python
# Trailing pragma after another comment - the pragma portion should not count
# toward reserved width in preview mode.
# toward reserved width.
# The expression is long enough that the formatter would break it if the full
# comment width were reserved, but short enough to fit if only the non-pragma
@@ -15,65 +15,33 @@ i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # comment
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # comment # pyright: ignore
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) ## noqa: F401
# Plain pragma (no nested comment) - should behave the same in stable and preview
# Plain pragma (no nested comment) - should not reserve any width
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: F401
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: ignore
# Not a pragma - should reserve full width in both modes
# Not a pragma - should reserve full width
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # comment # not a pragma
```
## Output
```python
# Trailing pragma after another comment - the pragma portion should not count
# toward reserved width in preview mode.
# toward reserved width.
# The expression is long enough that the formatter would break it if the full
# comment width were reserved, but short enough to fit if only the non-pragma
# prefix is reserved.
i = (
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
) # comment # noqa: F401
i = (
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
) # comment # type: ignore
i = (
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
) # comment # pyright: ignore
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # comment # noqa: F401
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # comment # type: ignore
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # comment # pyright: ignore
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) ## noqa: F401
# Plain pragma (no nested comment) - should behave the same in stable and preview
# Plain pragma (no nested comment) - should not reserve any width
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # noqa: F401
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # type: ignore
# Not a pragma - should reserve full width in both modes
# Not a pragma - should reserve full width
i = (
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
) # comment # not a pragma
```
## Preview changes
```diff
--- Stable
+++ Preview
@@ -4,15 +4,9 @@
# The expression is long enough that the formatter would break it if the full
# comment width were reserved, but short enough to fit if only the non-pragma
# prefix is reserved.
-i = (
- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
-) # comment # noqa: F401
-i = (
- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
-) # comment # type: ignore
-i = (
- "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
-) # comment # pyright: ignore
+i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # comment # noqa: F401
+i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # comment # type: ignore
+i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) # comment # pyright: ignore
i = ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",) ## noqa: F401
# Plain pragma (no nested comment) - should behave the same in stable and preview
```
+2
View File
@@ -39,6 +39,8 @@ has already been formatted by Black.
### Pragma comments are ignored when computing line width
Pragma comments (`# type`, `# noqa`, `# pyright`, `# pylint`, etc.) are ignored when computing the width of a line.
For mixed comments such as `# explanation # noqa: F401`, only the pragma suffix is ignored;
the preceding comment text still contributes to the line width.
This prevents Ruff from moving pragma comments around, thereby modifying their meaning and behavior:
See Ruff's [pragma comment handling proposal](https://github.com/astral-sh/ruff/discussions/6670)