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.
This commit is contained in:
Micha Reiser 2023-10-24 17:14:05 +09:00 committed by GitHub
parent 2587aef1ea
commit 9feb86caa4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 125 additions and 28 deletions

View file

@ -13,6 +13,7 @@ use ruff_linter::settings::types::{
};
use ruff_linter::{RuleParser, RuleSelector, RuleSelectorParser};
use ruff_workspace::configuration::{Configuration, RuleSelection};
use ruff_workspace::options::PycodestyleOptions;
use ruff_workspace::resolver::ConfigurationTransformer;
#[derive(Debug, Parser)]
@ -685,8 +686,12 @@ impl ConfigurationTransformer for CliOverrides {
if let Some(force_exclude) = &self.force_exclude {
config.force_exclude = Some(*force_exclude);
}
if let Some(line_length) = &self.line_length {
config.line_length = Some(*line_length);
if let Some(line_length) = self.line_length {
config.line_length = Some(line_length);
config.lint.pycodestyle = Some(PycodestyleOptions {
max_line_length: Some(line_length),
..config.lint.pycodestyle.unwrap_or_default()
});
}
if let Some(preview) = &self.preview {
config.preview = Some(*preview);

View file

@ -270,3 +270,41 @@ if __name__ == "__main__":
"###);
Ok(())
}
#[test]
fn line_too_long_width_override() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
line-length = 80
select = ["E501"]
[pycodestyle]
max-line-length = 100
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(&ruff_toml)
.args(["--stdin-filename", "test.py"])
.arg("-")
.pass_stdin(r#"
# longer than 80, but less than 100
_ = "---------------------------------------------------------------------------亜亜亜亜亜亜"
# longer than 100
_ = "---------------------------------------------------------------------------亜亜亜亜亜亜亜亜亜亜亜亜亜亜"
"#), @r###"
success: false
exit_code: 1
----- stdout -----
test.py:5:91: E501 Line too long (109 > 100)
Found 1 error.
----- stderr -----
"###);
Ok(())
}

View file

@ -95,6 +95,7 @@ mod tests {
use crate::line_width::LineLength;
use crate::registry::Rule;
use crate::rules::pycodestyle;
use crate::settings::LinterSettings;
use super::check_physical_lines;
@ -114,7 +115,10 @@ mod tests {
&indexer,
&[],
&LinterSettings {
line_length,
pycodestyle: pycodestyle::settings::Settings {
max_line_length: line_length,
..pycodestyle::settings::Settings::default()
},
..LinterSettings::for_rule(Rule::LineTooLong)
},
)

View file

@ -139,7 +139,7 @@ pub(crate) fn multiple_with_statements(
content,
with_stmt.into(),
checker.locator(),
checker.settings.line_length,
checker.settings.pycodestyle.max_line_length,
checker.settings.tab_size,
)
}) {

View file

@ -128,7 +128,7 @@ pub(crate) fn nested_if_statements(
content,
(&nested_if).into(),
checker.locator(),
checker.settings.line_length,
checker.settings.pycodestyle.max_line_length,
checker.settings.tab_size,
)
}) {

View file

@ -195,7 +195,7 @@ pub(crate) fn if_else_block_instead_of_dict_get(checker: &mut Checker, stmt_if:
&contents,
stmt_if.into(),
checker.locator(),
checker.settings.line_length,
checker.settings.pycodestyle.max_line_length,
checker.settings.tab_size,
) {
return;

View file

@ -130,7 +130,7 @@ pub(crate) fn if_else_block_instead_of_if_exp(checker: &mut Checker, stmt_if: &a
&contents,
stmt_if.into(),
checker.locator(),
checker.settings.line_length,
checker.settings.pycodestyle.max_line_length,
checker.settings.tab_size,
) {
return;

View file

@ -101,7 +101,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt) {
&contents,
stmt.into(),
checker.locator(),
checker.settings.line_length,
checker.settings.pycodestyle.max_line_length,
checker.settings.tab_size,
) {
return;
@ -188,7 +188,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt) {
.slice(TextRange::new(line_start, stmt.start())),
)
.add_str(&contents)
> checker.settings.line_length
> checker.settings.pycodestyle.max_line_length
{
return;
}

View file

@ -120,7 +120,7 @@ pub(crate) fn organize_imports(
block,
comments,
locator,
settings.line_length,
settings.pycodestyle.max_line_length,
LineWidthBuilder::new(settings.tab_size).add_str(indentation),
stylist,
&settings.src,

View file

@ -16,6 +16,7 @@ mod tests {
use crate::line_width::LineLength;
use crate::registry::Rule;
use crate::rules::pycodestyle;
use crate::settings::types::PreviewMode;
use crate::test::test_path;
use crate::{assert_messages, settings};
@ -229,7 +230,10 @@ mod tests {
Path::new("pycodestyle/E501_2.py"),
&settings::LinterSettings {
tab_size: NonZeroU8::new(tab_size).unwrap().into(),
line_length: LineLength::try_from(6).unwrap(),
pycodestyle: pycodestyle::settings::Settings {
max_line_length: LineLength::try_from(6).unwrap(),
..pycodestyle::settings::Settings::default()
},
..settings::LinterSettings::for_rule(Rule::LineTooLong)
},
)?;

View file

@ -47,6 +47,7 @@ use crate::settings::LinterSettings;
///
/// ## Options
/// - `line-length`
/// - `pycodestyle.max-line-length`
/// - `task-tags`
/// - `pycodestyle.ignore-overlong-task-comments`
///
@ -68,7 +69,7 @@ pub(crate) fn line_too_long(
indexer: &Indexer,
settings: &LinterSettings,
) -> Option<Diagnostic> {
let limit = settings.line_length;
let limit = settings.pycodestyle.max_line_length;
Overlong::try_from_line(
line,

View file

@ -6,6 +6,7 @@ use crate::line_width::LineLength;
#[derive(Debug, Default, CacheKey)]
pub struct Settings {
pub max_line_length: LineLength,
pub max_doc_length: Option<LineLength>,
pub ignore_overlong_task_comments: bool,
}

View file

@ -399,7 +399,7 @@ pub(crate) fn f_strings(
&contents,
template.into(),
checker.locator(),
checker.settings.line_length,
checker.settings.pycodestyle.max_line_length,
checker.settings.tab_size,
) {
return;

View file

@ -26,7 +26,7 @@ use crate::rules::{
use crate::settings::types::{FilePatternSet, PerFileIgnore, PythonVersion};
use crate::{codes, RuleSelector};
use super::line_width::{LineLength, TabSize};
use super::line_width::TabSize;
use self::rule_table::RuleTable;
use self::types::PreviewMode;
@ -56,7 +56,6 @@ pub struct LinterSettings {
pub dummy_variable_rgx: Regex,
pub external: FxHashSet<String>,
pub ignore_init_module_imports: bool,
pub line_length: LineLength,
pub logger_objects: Vec<String>,
pub namespace_packages: Vec<PathBuf>,
pub src: Vec<PathBuf>,
@ -147,7 +146,6 @@ impl LinterSettings {
external: HashSet::default(),
ignore_init_module_imports: false,
line_length: LineLength::default(),
logger_objects: vec![],
namespace_packages: vec![],

View file

@ -21,6 +21,7 @@ use ruff_linter::line_width::{LineLength, TabSize};
use ruff_linter::registry::RuleNamespace;
use ruff_linter::registry::{Rule, RuleSet, INCOMPATIBLE_CODES};
use ruff_linter::rule_selector::{PreviewOptions, Specificity};
use ruff_linter::rules::pycodestyle;
use ruff_linter::settings::rule_table::RuleTable;
use ruff_linter::settings::types::{
FilePattern, FilePatternSet, PerFileIgnore, PreviewMode, PythonVersion, SerializationFormat,
@ -183,6 +184,8 @@ impl Configuration {
let lint = self.lint;
let lint_preview = lint.preview.unwrap_or(global_preview);
let line_length = self.line_length.unwrap_or_default();
Ok(Settings {
cache_dir: self
.cache_dir
@ -225,7 +228,6 @@ impl Configuration {
.unwrap_or_else(|| DUMMY_VARIABLE_RGX.clone()),
external: FxHashSet::from_iter(lint.external.unwrap_or_default()),
ignore_init_module_imports: lint.ignore_init_module_imports.unwrap_or_default(),
line_length: self.line_length.unwrap_or_default(),
tab_size: self.tab_size.unwrap_or_default(),
namespace_packages: self.namespace_packages.unwrap_or_default(),
per_file_ignores: resolve_per_file_ignores(
@ -346,10 +348,14 @@ impl Configuration {
.map(Pep8NamingOptions::try_into_settings)
.transpose()?
.unwrap_or_default(),
pycodestyle: lint
.pycodestyle
.map(PycodestyleOptions::into_settings)
.unwrap_or_default(),
pycodestyle: if let Some(pycodestyle) = lint.pycodestyle {
pycodestyle.into_settings(line_length)
} else {
pycodestyle::settings::Settings {
max_line_length: line_length,
..pycodestyle::settings::Settings::default()
}
},
pydocstyle: lint
.pydocstyle
.map(PydocstyleOptions::into_settings)

View file

@ -60,7 +60,7 @@ pub struct Options {
/// this base configuration file, then merge in any properties defined
/// in the current configuration file.
#[option(
default = r#"None"#,
default = r#"null"#,
value_type = "str",
example = r#"
# Extend the `pyproject.toml` file in the parent directory.
@ -132,7 +132,7 @@ pub struct Options {
/// results across many environments, e.g., with a `pyproject.toml`
/// file).
#[option(
default = "None",
default = "null",
value_type = "str",
example = r#"
required-version = "0.0.193"
@ -362,6 +362,8 @@ pub struct Options {
/// Note: While the formatter will attempt to format lines such that they remain
/// within the `line-length`, it isn't a hard upper bound, and formatted lines may
/// exceed the `line-length`.
///
/// See [`pycodestyle.max-line-length`](#pycodestyle-max-line-length) to configure different lengths for `E501` and the formatter.
#[option(
default = "88",
value_type = "int",
@ -1043,7 +1045,7 @@ pub struct Flake8CopyrightOptions {
/// Author to enforce within the copyright notice. If provided, the
/// author must be present immediately following the copyright notice.
#[option(default = "None", value_type = "str", example = r#"author = "Ruff""#)]
#[option(default = "null", value_type = "str", example = r#"author = "Ruff""#)]
pub author: Option<String>,
/// A minimum file size (in bytes) required for a copyright notice to
@ -2247,6 +2249,32 @@ impl Pep8NamingOptions {
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct PycodestyleOptions {
/// The maximum line length to allow for [`line-too-long`](https://docs.astral.sh/ruff/rules/line-too-long/) violations. By default,
/// this is set to the value of the [`line-length`](#line-length) option.
///
/// Use this option when you want to detect extra-long lines that the formatter can't automatically split by setting
/// `pycodestyle.line-length` to a value larger than [`line-length`](#line-length).
///
/// ```toml
/// line-length = 88 # The formatter wraps lines at a length of 88
///
/// [pycodestyle]
/// max-line-length = 100 # E501 reports lines that exceed the length of 100.
/// ```
///
/// The length is determined by the number of characters per line, except for lines containing East Asian characters or emojis.
/// For these lines, the [unicode width](https://unicode.org/reports/tr11/) of each character is added up to determine the length.
///
/// See the [`line-too-long`](https://docs.astral.sh/ruff/rules/line-too-long/) rule for more information.
#[option(
default = "null",
value_type = "int",
example = r#"
max-line-length = 100
"#
)]
pub max_line_length: Option<LineLength>,
/// The maximum line length to allow for [`doc-line-too-long`](https://docs.astral.sh/ruff/rules/doc-line-too-long/) violations within
/// documentation (`W505`), including standalone comments. By default,
/// this is set to null which disables reporting violations.
@ -2256,7 +2284,7 @@ pub struct PycodestyleOptions {
///
/// See the [`doc-line-too-long`](https://docs.astral.sh/ruff/rules/doc-line-too-long/) rule for more information.
#[option(
default = "None",
default = "null",
value_type = "int",
example = r#"
max-doc-length = 88
@ -2278,9 +2306,10 @@ pub struct PycodestyleOptions {
}
impl PycodestyleOptions {
pub fn into_settings(self) -> pycodestyle::settings::Settings {
pub fn into_settings(self, global_line_length: LineLength) -> pycodestyle::settings::Settings {
pycodestyle::settings::Settings {
max_doc_length: self.max_doc_length,
max_line_length: self.max_line_length.unwrap_or(global_line_length),
ignore_overlong_task_comments: self.ignore_overlong_task_comments.unwrap_or_default(),
}
}
@ -2321,7 +2350,7 @@ pub struct PydocstyleOptions {
/// enabling _additional_ rules on top of a convention is currently
/// unsupported.
#[option(
default = r#"None"#,
default = r#"null"#,
value_type = r#""google" | "numpy" | "pep257""#,
example = r#"
# Use Google-style docstrings.

13
ruff.schema.json generated
View file

@ -425,7 +425,7 @@
]
},
"line-length": {
"description": "The line length to use when enforcing long-lines violations (like `E501`) and at which the formatter prefers to wrap lines.\n\nThe length is determined by the number of characters per line, except for lines containing East Asian characters or emojis. For these lines, the [unicode width](https://unicode.org/reports/tr11/) of each character is added up to determine the length.\n\nThe value must be greater than `0` and less than or equal to `320`.\n\nNote: While the formatter will attempt to format lines such that they remain within the `line-length`, it isn't a hard upper bound, and formatted lines may exceed the `line-length`.",
"description": "The line length to use when enforcing long-lines violations (like `E501`) and at which the formatter prefers to wrap lines.\n\nThe length is determined by the number of characters per line, except for lines containing East Asian characters or emojis. For these lines, the [unicode width](https://unicode.org/reports/tr11/) of each character is added up to determine the length.\n\nThe value must be greater than `0` and less than or equal to `320`.\n\nNote: While the formatter will attempt to format lines such that they remain within the `line-length`, it isn't a hard upper bound, and formatted lines may exceed the `line-length`.\n\nSee [`pycodestyle.max-line-length`](#pycodestyle-max-line-length) to configure different lengths for `E501` and the formatter.",
"anyOf": [
{
"$ref": "#/definitions/LineLength"
@ -2204,6 +2204,17 @@
"type": "null"
}
]
},
"max-line-length": {
"description": "The maximum line length to allow for [`line-too-long`](https://docs.astral.sh/ruff/rules/line-too-long/) violations. By default, this is set to the value of the [`line-length`](#line-length) option.\n\nUse this option when you want to detect extra-long lines that the formatter can't automatically split by setting `pycodestyle.line-length` to a value larger than [`line-length`](#line-length).\n\n```toml line-length = 88 # The formatter wraps lines at a length of 88\n\n[pycodestyle] max-line-length = 100 # E501 reports lines that exceed the length of 100. ```\n\nThe length is determined by the number of characters per line, except for lines containing East Asian characters or emojis. For these lines, the [unicode width](https://unicode.org/reports/tr11/) of each character is added up to determine the length.\n\nSee the [`line-too-long`](https://docs.astral.sh/ruff/rules/line-too-long/) rule for more information.",
"anyOf": [
{
"$ref": "#/definitions/LineLength"
},
{
"type": "null"
}
]
}
},
"additionalProperties": false