Skip to content

Commit 9feb86c

Browse files
authored
New pycodestyle.max-line-length option (#8039)
## Summary This PR introduces a new `pycodestyl.max-line-length` option that allows overriding the global `line-length` option for `E501` only. This is useful when using the formatter and `E501` together, where the formatter uses a lower limit and `E501` is only used to catch extra-long lines. Closes #7644 ## Considerations ~~Our fix infrastructure asserts in some places that the fix doesn't exceed the configured `line-width`. With this change, the question is whether it should use the `pycodestyle.max-line-width` or `line-width` option to make that decision. I opted for the global `line-width` for now, considering that it should be the lower limit. However, this constraint isn't enforced and users not using the formatter may only specify `pycodestyle.max-line-width` because they're unaware of the global option (and it solves their need).~~ ~~I'm interested to hear your thoughts on whether we should use `pycodestyle.max-line-width` or `line-width` to decide on whether to emit a fix or not.~~ Edit: The linter users `pycodestyle.max-line-width`. The `line-width` option has been removed from the `LinterSettings` ## Test Plan Added integration test. Built the documentation and verified that the links are correct.
1 parent 2587aef commit 9feb86c

File tree

17 files changed

+125
-28
lines changed

17 files changed

+125
-28
lines changed

crates/ruff_cli/src/args.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use ruff_linter::settings::types::{
1313
};
1414
use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser};
1515
use ruff_workspace::configuration::{Configuration, RuleSelection};
16+
use ruff_workspace::options::PycodestyleOptions;
1617
use ruff_workspace::resolver::ConfigurationTransformer;
1718

1819
#[derive(Debug, Parser)]
@@ -685,8 +686,12 @@ impl ConfigurationTransformer for CliOverrides {
685686
if let Some(force_exclude) = &self.force_exclude {
686687
config.force_exclude = Some(*force_exclude);
687688
}
688-
if let Some(line_length) = &self.line_length {
689-
config.line_length = Some(*line_length);
689+
if let Some(line_length) = self.line_length {
690+
config.line_length = Some(line_length);
691+
config.lint.pycodestyle = Some(PycodestyleOptions {
692+
max_line_length: Some(line_length),
693+
..config.lint.pycodestyle.unwrap_or_default()
694+
});
690695
}
691696
if let Some(preview) = &self.preview {
692697
config.preview = Some(*preview);

crates/ruff_cli/tests/lint.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,3 +270,41 @@ if __name__ == "__main__":
270270
"###);
271271
Ok(())
272272
}
273+
274+
#[test]
275+
fn line_too_long_width_override() -> Result<()> {
276+
let tempdir = TempDir::new()?;
277+
let ruff_toml = tempdir.path().join("ruff.toml");
278+
fs::write(
279+
&ruff_toml,
280+
r#"
281+
line-length = 80
282+
select = ["E501"]
283+
284+
[pycodestyle]
285+
max-line-length = 100
286+
"#,
287+
)?;
288+
289+
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
290+
.args(STDIN_BASE_OPTIONS)
291+
.arg("--config")
292+
.arg(&ruff_toml)
293+
.args(["--stdin-filename", "test.py"])
294+
.arg("-")
295+
.pass_stdin(r#"
296+
# longer than 80, but less than 100
297+
_ = "---------------------------------------------------------------------------亜亜亜亜亜亜"
298+
# longer than 100
299+
_ = "---------------------------------------------------------------------------亜亜亜亜亜亜亜亜亜亜亜亜亜亜"
300+
"#), @r###"
301+
success: false
302+
exit_code: 1
303+
----- stdout -----
304+
test.py:5:91: E501 Line too long (109 > 100)
305+
Found 1 error.
306+
307+
----- stderr -----
308+
"###);
309+
Ok(())
310+
}

crates/ruff_linter/src/checkers/physical_lines.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ mod tests {
9595

9696
use crate::line_width::LineLength;
9797
use crate::registry::Rule;
98+
use crate::rules::pycodestyle;
9899
use crate::settings::LinterSettings;
99100

100101
use super::check_physical_lines;
@@ -114,7 +115,10 @@ mod tests {
114115
&indexer,
115116
&[],
116117
&LinterSettings {
117-
line_length,
118+
pycodestyle: pycodestyle::settings::Settings {
119+
max_line_length: line_length,
120+
..pycodestyle::settings::Settings::default()
121+
},
118122
..LinterSettings::for_rule(Rule::LineTooLong)
119123
},
120124
)

crates/ruff_linter/src/rules/flake8_simplify/rules/ast_with.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ pub(crate) fn multiple_with_statements(
139139
content,
140140
with_stmt.into(),
141141
checker.locator(),
142-
checker.settings.line_length,
142+
checker.settings.pycodestyle.max_line_length,
143143
checker.settings.tab_size,
144144
)
145145
}) {

crates/ruff_linter/src/rules/flake8_simplify/rules/collapsible_if.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ pub(crate) fn nested_if_statements(
128128
content,
129129
(&nested_if).into(),
130130
checker.locator(),
131-
checker.settings.line_length,
131+
checker.settings.pycodestyle.max_line_length,
132132
checker.settings.tab_size,
133133
)
134134
}) {

crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_dict_get.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &mut Checker, stmt_if:
195195
&contents,
196196
stmt_if.into(),
197197
checker.locator(),
198-
checker.settings.line_length,
198+
checker.settings.pycodestyle.max_line_length,
199199
checker.settings.tab_size,
200200
) {
201201
return;

crates/ruff_linter/src/rules/flake8_simplify/rules/if_else_block_instead_of_if_exp.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ pub(crate) fn if_else_block_instead_of_if_exp(checker: &mut Checker, stmt_if: &a
130130
&contents,
131131
stmt_if.into(),
132132
checker.locator(),
133-
checker.settings.line_length,
133+
checker.settings.pycodestyle.max_line_length,
134134
checker.settings.tab_size,
135135
) {
136136
return;

crates/ruff_linter/src/rules/flake8_simplify/rules/reimplemented_builtin.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt) {
101101
&contents,
102102
stmt.into(),
103103
checker.locator(),
104-
checker.settings.line_length,
104+
checker.settings.pycodestyle.max_line_length,
105105
checker.settings.tab_size,
106106
) {
107107
return;
@@ -188,7 +188,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt) {
188188
.slice(TextRange::new(line_start, stmt.start())),
189189
)
190190
.add_str(&contents)
191-
> checker.settings.line_length
191+
> checker.settings.pycodestyle.max_line_length
192192
{
193193
return;
194194
}

crates/ruff_linter/src/rules/isort/rules/organize_imports.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ pub(crate) fn organize_imports(
120120
block,
121121
comments,
122122
locator,
123-
settings.line_length,
123+
settings.pycodestyle.max_line_length,
124124
LineWidthBuilder::new(settings.tab_size).add_str(indentation),
125125
stylist,
126126
&settings.src,

crates/ruff_linter/src/rules/pycodestyle/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ mod tests {
1616

1717
use crate::line_width::LineLength;
1818
use crate::registry::Rule;
19+
use crate::rules::pycodestyle;
1920
use crate::settings::types::PreviewMode;
2021
use crate::test::test_path;
2122
use crate::{assert_messages, settings};
@@ -229,7 +230,10 @@ mod tests {
229230
Path::new("pycodestyle/E501_2.py"),
230231
&settings::LinterSettings {
231232
tab_size: NonZeroU8::new(tab_size).unwrap().into(),
232-
line_length: LineLength::try_from(6).unwrap(),
233+
pycodestyle: pycodestyle::settings::Settings {
234+
max_line_length: LineLength::try_from(6).unwrap(),
235+
..pycodestyle::settings::Settings::default()
236+
},
233237
..settings::LinterSettings::for_rule(Rule::LineTooLong)
234238
},
235239
)?;

crates/ruff_linter/src/rules/pycodestyle/rules/line_too_long.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ use crate::settings::LinterSettings;
4747
///
4848
/// ## Options
4949
/// - `line-length`
50+
/// - `pycodestyle.max-line-length`
5051
/// - `task-tags`
5152
/// - `pycodestyle.ignore-overlong-task-comments`
5253
///
@@ -68,7 +69,7 @@ pub(crate) fn line_too_long(
6869
indexer: &Indexer,
6970
settings: &LinterSettings,
7071
) -> Option<Diagnostic> {
71-
let limit = settings.line_length;
72+
let limit = settings.pycodestyle.max_line_length;
7273

7374
Overlong::try_from_line(
7475
line,

crates/ruff_linter/src/rules/pycodestyle/settings.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::line_width::LineLength;
66

77
#[derive(Debug, Default, CacheKey)]
88
pub struct Settings {
9+
pub max_line_length: LineLength,
910
pub max_doc_length: Option<LineLength>,
1011
pub ignore_overlong_task_comments: bool,
1112
}

crates/ruff_linter/src/rules/pyupgrade/rules/f_strings.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -399,7 +399,7 @@ pub(crate) fn f_strings(
399399
&contents,
400400
template.into(),
401401
checker.locator(),
402-
checker.settings.line_length,
402+
checker.settings.pycodestyle.max_line_length,
403403
checker.settings.tab_size,
404404
) {
405405
return;

crates/ruff_linter/src/settings/mod.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use crate::rules::{
2626
use crate::settings::types::{FilePatternSet, PerFileIgnore, PythonVersion};
2727
use crate::{codes, RuleSelector};
2828

29-
use super::line_width::{LineLength, TabSize};
29+
use super::line_width::TabSize;
3030

3131
use self::rule_table::RuleTable;
3232
use self::types::PreviewMode;
@@ -56,7 +56,6 @@ pub struct LinterSettings {
5656
pub dummy_variable_rgx: Regex,
5757
pub external: FxHashSet<String>,
5858
pub ignore_init_module_imports: bool,
59-
pub line_length: LineLength,
6059
pub logger_objects: Vec<String>,
6160
pub namespace_packages: Vec<PathBuf>,
6261
pub src: Vec<PathBuf>,
@@ -147,7 +146,6 @@ impl LinterSettings {
147146

148147
external: HashSet::default(),
149148
ignore_init_module_imports: false,
150-
line_length: LineLength::default(),
151149
logger_objects: vec![],
152150
namespace_packages: vec![],
153151

crates/ruff_workspace/src/configuration.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use ruff_linter::line_width::{LineLength, TabSize};
2121
use ruff_linter::registry::RuleNamespace;
2222
use ruff_linter::registry::{Rule, RuleSet, INCOMPATIBLE_CODES};
2323
use ruff_linter::rule_selector::{PreviewOptions, Specificity};
24+
use ruff_linter::rules::pycodestyle;
2425
use ruff_linter::settings::rule_table::RuleTable;
2526
use ruff_linter::settings::types::{
2627
FilePattern, FilePatternSet, PerFileIgnore, PreviewMode, PythonVersion, SerializationFormat,
@@ -183,6 +184,8 @@ impl Configuration {
183184
let lint = self.lint;
184185
let lint_preview = lint.preview.unwrap_or(global_preview);
185186

187+
let line_length = self.line_length.unwrap_or_default();
188+
186189
Ok(Settings {
187190
cache_dir: self
188191
.cache_dir
@@ -225,7 +228,6 @@ impl Configuration {
225228
.unwrap_or_else(|| DUMMY_VARIABLE_RGX.clone()),
226229
external: FxHashSet::from_iter(lint.external.unwrap_or_default()),
227230
ignore_init_module_imports: lint.ignore_init_module_imports.unwrap_or_default(),
228-
line_length: self.line_length.unwrap_or_default(),
229231
tab_size: self.tab_size.unwrap_or_default(),
230232
namespace_packages: self.namespace_packages.unwrap_or_default(),
231233
per_file_ignores: resolve_per_file_ignores(
@@ -346,10 +348,14 @@ impl Configuration {
346348
.map(Pep8NamingOptions::try_into_settings)
347349
.transpose()?
348350
.unwrap_or_default(),
349-
pycodestyle: lint
350-
.pycodestyle
351-
.map(PycodestyleOptions::into_settings)
352-
.unwrap_or_default(),
351+
pycodestyle: if let Some(pycodestyle) = lint.pycodestyle {
352+
pycodestyle.into_settings(line_length)
353+
} else {
354+
pycodestyle::settings::Settings {
355+
max_line_length: line_length,
356+
..pycodestyle::settings::Settings::default()
357+
}
358+
},
353359
pydocstyle: lint
354360
.pydocstyle
355361
.map(PydocstyleOptions::into_settings)

crates/ruff_workspace/src/options.rs

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ pub struct Options {
6060
/// this base configuration file, then merge in any properties defined
6161
/// in the current configuration file.
6262
#[option(
63-
default = r#"None"#,
63+
default = r#"null"#,
6464
value_type = "str",
6565
example = r#"
6666
# Extend the `pyproject.toml` file in the parent directory.
@@ -132,7 +132,7 @@ pub struct Options {
132132
/// results across many environments, e.g., with a `pyproject.toml`
133133
/// file).
134134
#[option(
135-
default = "None",
135+
default = "null",
136136
value_type = "str",
137137
example = r#"
138138
required-version = "0.0.193"
@@ -362,6 +362,8 @@ pub struct Options {
362362
/// Note: While the formatter will attempt to format lines such that they remain
363363
/// within the `line-length`, it isn't a hard upper bound, and formatted lines may
364364
/// exceed the `line-length`.
365+
///
366+
/// See [`pycodestyle.max-line-length`](#pycodestyle-max-line-length) to configure different lengths for `E501` and the formatter.
365367
#[option(
366368
default = "88",
367369
value_type = "int",
@@ -1043,7 +1045,7 @@ pub struct Flake8CopyrightOptions {
10431045

10441046
/// Author to enforce within the copyright notice. If provided, the
10451047
/// author must be present immediately following the copyright notice.
1046-
#[option(default = "None", value_type = "str", example = r#"author = "Ruff""#)]
1048+
#[option(default = "null", value_type = "str", example = r#"author = "Ruff""#)]
10471049
pub author: Option<String>,
10481050

10491051
/// A minimum file size (in bytes) required for a copyright notice to
@@ -2247,6 +2249,32 @@ impl Pep8NamingOptions {
22472249
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
22482250
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
22492251
pub struct PycodestyleOptions {
2252+
/// The maximum line length to allow for [`line-too-long`](https://docs.astral.sh/ruff/rules/line-too-long/) violations. By default,
2253+
/// this is set to the value of the [`line-length`](#line-length) option.
2254+
///
2255+
/// Use this option when you want to detect extra-long lines that the formatter can't automatically split by setting
2256+
/// `pycodestyle.line-length` to a value larger than [`line-length`](#line-length).
2257+
///
2258+
/// ```toml
2259+
/// line-length = 88 # The formatter wraps lines at a length of 88
2260+
///
2261+
/// [pycodestyle]
2262+
/// max-line-length = 100 # E501 reports lines that exceed the length of 100.
2263+
/// ```
2264+
///
2265+
/// The length is determined by the number of characters per line, except for lines containing East Asian characters or emojis.
2266+
/// For these lines, the [unicode width](https://unicode.org/reports/tr11/) of each character is added up to determine the length.
2267+
///
2268+
/// See the [`line-too-long`](https://docs.astral.sh/ruff/rules/line-too-long/) rule for more information.
2269+
#[option(
2270+
default = "null",
2271+
value_type = "int",
2272+
example = r#"
2273+
max-line-length = 100
2274+
"#
2275+
)]
2276+
pub max_line_length: Option<LineLength>,
2277+
22502278
/// The maximum line length to allow for [`doc-line-too-long`](https://docs.astral.sh/ruff/rules/doc-line-too-long/) violations within
22512279
/// documentation (`W505`), including standalone comments. By default,
22522280
/// this is set to null which disables reporting violations.
@@ -2256,7 +2284,7 @@ pub struct PycodestyleOptions {
22562284
///
22572285
/// See the [`doc-line-too-long`](https://docs.astral.sh/ruff/rules/doc-line-too-long/) rule for more information.
22582286
#[option(
2259-
default = "None",
2287+
default = "null",
22602288
value_type = "int",
22612289
example = r#"
22622290
max-doc-length = 88
@@ -2278,9 +2306,10 @@ pub struct PycodestyleOptions {
22782306
}
22792307

22802308
impl PycodestyleOptions {
2281-
pub fn into_settings(self) -> pycodestyle::settings::Settings {
2309+
pub fn into_settings(self, global_line_length: LineLength) -> pycodestyle::settings::Settings {
22822310
pycodestyle::settings::Settings {
22832311
max_doc_length: self.max_doc_length,
2312+
max_line_length: self.max_line_length.unwrap_or(global_line_length),
22842313
ignore_overlong_task_comments: self.ignore_overlong_task_comments.unwrap_or_default(),
22852314
}
22862315
}
@@ -2321,7 +2350,7 @@ pub struct PydocstyleOptions {
23212350
/// enabling _additional_ rules on top of a convention is currently
23222351
/// unsupported.
23232352
#[option(
2324-
default = r#"None"#,
2353+
default = r#"null"#,
23252354
value_type = r#""google" | "numpy" | "pep257""#,
23262355
example = r#"
23272356
# Use Google-style docstrings.

ruff.schema.json

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)