mirror of
https://github.com/astral-sh/ruff.git
synced 2025-12-23 09:19:39 +00:00
Support isort's force-single-line option (#1366)
This commit is contained in:
parent
e692c4a2cc
commit
534d8d049c
10 changed files with 210 additions and 4 deletions
34
README.md
34
README.md
|
|
@ -2468,6 +2468,23 @@ extra-standard-library = ["path"]
|
|||
|
||||
---
|
||||
|
||||
#### [`force-single-line`](#force-single-line)
|
||||
|
||||
Forces all from imports to appear on their own line.
|
||||
|
||||
**Default value**: `false`
|
||||
|
||||
**Type**: `bool`
|
||||
|
||||
**Example usage**:
|
||||
|
||||
```toml
|
||||
[tool.ruff.isort]
|
||||
force-single-line = true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### [`force-wrap-aliases`](#force-wrap-aliases)
|
||||
|
||||
Force `import from` statements with multiple members and at least one
|
||||
|
|
@ -2537,6 +2554,23 @@ known-third-party = ["src"]
|
|||
|
||||
---
|
||||
|
||||
#### [`single-line-exclusions`](#single-line-exclusions)
|
||||
|
||||
One or more modules to exclude from the single line rule.
|
||||
|
||||
**Default value**: `[]`
|
||||
|
||||
**Type**: `Vec<String>`
|
||||
|
||||
**Example usage**:
|
||||
|
||||
```toml
|
||||
[tool.ruff.isort]
|
||||
single-line-exclusions = ["os", "json"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### [`split-on-trailing-comma`](#split-on-trailing-comma)
|
||||
|
||||
If a comma is placed after the last member in a multi-line import, then
|
||||
|
|
|
|||
|
|
@ -167,6 +167,11 @@ export const AVAILABLE_OPTIONS: OptionGroup[] = [
|
|||
"default": '[]',
|
||||
"type": 'Vec<String>',
|
||||
},
|
||||
{
|
||||
"name": "force-single-line",
|
||||
"default": 'false',
|
||||
"type": 'bool',
|
||||
},
|
||||
{
|
||||
"name": "force-wrap-aliases",
|
||||
"default": 'false',
|
||||
|
|
@ -182,6 +187,11 @@ export const AVAILABLE_OPTIONS: OptionGroup[] = [
|
|||
"default": '[]',
|
||||
"type": 'Vec<String>',
|
||||
},
|
||||
{
|
||||
"name": "single-line-exclusions",
|
||||
"default": '[]',
|
||||
"type": 'Vec<String>',
|
||||
},
|
||||
{
|
||||
"name": "split-on-trailing-comma",
|
||||
"default": 'true',
|
||||
|
|
|
|||
|
|
@ -38,3 +38,5 @@ strip = true
|
|||
[tool.ruff.isort]
|
||||
force-wrap-aliases = true
|
||||
combine-as-imports = true
|
||||
force-single-line = true
|
||||
single-line-exclusions = ["os", "logging.handlers"]
|
||||
|
|
|
|||
18
resources/test/fixtures/isort/force_single_line.py
vendored
Normal file
18
resources/test/fixtures/isort/force_single_line.py
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import sys, math
|
||||
from os import path, uname
|
||||
from logging.handlers import StreamHandler, FileHandler
|
||||
|
||||
# comment 1
|
||||
from third_party import lib1, lib2, \
|
||||
lib3, lib7, lib5, lib6
|
||||
# comment 2
|
||||
from third_party import lib4
|
||||
|
||||
from foo import bar # comment 3
|
||||
from foo2 import bar2 # comment 4
|
||||
|
||||
# comment 5
|
||||
from bar import (
|
||||
a, # comment 6
|
||||
b, # comment 7
|
||||
)
|
||||
|
|
@ -1080,6 +1080,13 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"force-single-line": {
|
||||
"description": "Forces all from imports to appear on their own line.",
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"force-wrap-aliases": {
|
||||
"description": "Force `import from` statements with multiple members and at least one alias (e.g., `import A as B`) to wrap such that every line contains exactly one member. For example, this formatting would be retained, rather than condensing to a single line:\n\n```py from .utils import ( test_directory as test_directory, test_id as test_id ) ```\n\nNote that this setting is only effective when combined with `combine-as-imports = true`. When `combine-as-imports` isn't enabled, every aliased `import from` will be given its own line, in which case, wrapping is not necessary.",
|
||||
"type": [
|
||||
|
|
@ -1107,6 +1114,16 @@
|
|||
"type": "string"
|
||||
}
|
||||
},
|
||||
"single-line-exclusions": {
|
||||
"description": "One or more modules to exclude from the single line rule.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"split-on-trailing-comma": {
|
||||
"description": "If a comma is placed after the last member in a multi-line import, then the imports will never be folded into one line.\n\nSee isort's [`split-on-trailing-comma`](https://pycqa.github.io/isort/docs/configuration/options.html#split-on-trailing-comma) option.",
|
||||
"type": [
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use std::cmp::Ordering;
|
|||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use itertools::Either::{Left, Right};
|
||||
use itertools::Itertools;
|
||||
use ropey::RopeBuilder;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
|
@ -495,7 +496,54 @@ fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
|
|||
ordered
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn force_single_line_imports<'a>(
|
||||
block: OrderedImportBlock<'a>,
|
||||
single_line_exclusions: &BTreeSet<String>,
|
||||
) -> OrderedImportBlock<'a> {
|
||||
OrderedImportBlock {
|
||||
import: block.import,
|
||||
import_from: block
|
||||
.import_from
|
||||
.into_iter()
|
||||
.flat_map(|(from_data, comment_set, trailing_comma, alias_data)| {
|
||||
if from_data
|
||||
.module
|
||||
.map_or(false, |module| single_line_exclusions.contains(module))
|
||||
{
|
||||
Left(std::iter::once((
|
||||
from_data,
|
||||
comment_set,
|
||||
trailing_comma,
|
||||
alias_data,
|
||||
)))
|
||||
} else {
|
||||
Right(
|
||||
alias_data
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(move |(index, alias_data)| {
|
||||
(
|
||||
from_data.clone(),
|
||||
if index == 0 {
|
||||
comment_set.clone()
|
||||
} else {
|
||||
CommentSet {
|
||||
atop: vec![],
|
||||
inline: vec![],
|
||||
}
|
||||
},
|
||||
TrailingComma::Absent,
|
||||
vec![alias_data],
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
|
||||
pub fn format_imports(
|
||||
block: &Block,
|
||||
comments: Vec<Comment>,
|
||||
|
|
@ -509,6 +557,8 @@ pub fn format_imports(
|
|||
combine_as_imports: bool,
|
||||
force_wrap_aliases: bool,
|
||||
split_on_trailing_comma: bool,
|
||||
force_single_line: bool,
|
||||
single_line_exclusions: &BTreeSet<String>,
|
||||
) -> String {
|
||||
let trailer = &block.trailer;
|
||||
let block = annotate_imports(&block.imports, comments, locator, split_on_trailing_comma);
|
||||
|
|
@ -531,7 +581,10 @@ pub fn format_imports(
|
|||
// Generate replacement source code.
|
||||
let mut is_first_block = true;
|
||||
for import_block in block_by_type.into_values() {
|
||||
let import_block = sort_imports(import_block);
|
||||
let mut import_block = sort_imports(import_block);
|
||||
if force_single_line {
|
||||
import_block = force_single_line_imports(import_block, single_line_exclusions);
|
||||
}
|
||||
|
||||
// Add a blank line between every section.
|
||||
if is_first_block {
|
||||
|
|
@ -577,6 +630,7 @@ pub fn format_imports(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::BTreeSet;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
|
|
@ -697,4 +751,28 @@ mod tests {
|
|||
insta::assert_yaml_snapshot!(snapshot, checks);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Path::new("force_single_line.py"))]
|
||||
fn force_single_line(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("force_single_line_{}", path.to_string_lossy());
|
||||
let mut checks = test_path(
|
||||
Path::new("./resources/test/fixtures/isort")
|
||||
.join(path)
|
||||
.as_path(),
|
||||
&Settings {
|
||||
isort: isort::settings::Settings {
|
||||
force_single_line: true,
|
||||
single_line_exclusions: vec!["os".to_string(), "logging.handlers".to_string()]
|
||||
.into_iter()
|
||||
.collect::<BTreeSet<_>>(),
|
||||
..isort::settings::Settings::default()
|
||||
},
|
||||
src: vec![Path::new("resources/test/fixtures/isort").to_path_buf()],
|
||||
..Settings::for_rule(CheckCode::I001)
|
||||
},
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(snapshot, checks);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,6 +82,8 @@ pub fn check_imports(
|
|||
settings.isort.combine_as_imports,
|
||||
settings.isort.force_wrap_aliases,
|
||||
settings.isort.split_on_trailing_comma,
|
||||
settings.isort.force_single_line,
|
||||
&settings.isort.single_line_exclusions,
|
||||
);
|
||||
|
||||
// Expand the span the entire range, including leading and trailing space.
|
||||
|
|
|
|||
|
|
@ -40,6 +40,22 @@ pub struct Options {
|
|||
/// enabled, every aliased `import from` will be given its own line, in
|
||||
/// which case, wrapping is not necessary.
|
||||
pub force_wrap_aliases: Option<bool>,
|
||||
#[option(
|
||||
default = r#"false"#,
|
||||
value_type = "bool",
|
||||
example = r#"force-single-line = true"#
|
||||
)]
|
||||
/// Forces all from imports to appear on their own line.
|
||||
pub force_single_line: Option<bool>,
|
||||
#[option(
|
||||
default = r#"[]"#,
|
||||
value_type = "Vec<String>",
|
||||
example = r#"
|
||||
single-line-exclusions = ["os", "json"]
|
||||
"#
|
||||
)]
|
||||
/// One or more modules to exclude from the single line rule.
|
||||
pub single_line_exclusions: Option<Vec<String>>,
|
||||
#[option(
|
||||
default = r#"false"#,
|
||||
value_type = "bool",
|
||||
|
|
@ -95,10 +111,13 @@ pub struct Options {
|
|||
}
|
||||
|
||||
#[derive(Debug, Hash)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
pub struct Settings {
|
||||
pub combine_as_imports: bool,
|
||||
pub force_wrap_aliases: bool,
|
||||
pub split_on_trailing_comma: bool,
|
||||
pub force_single_line: bool,
|
||||
pub single_line_exclusions: BTreeSet<String>,
|
||||
pub known_first_party: BTreeSet<String>,
|
||||
pub known_third_party: BTreeSet<String>,
|
||||
pub extra_standard_library: BTreeSet<String>,
|
||||
|
|
@ -110,6 +129,10 @@ impl Settings {
|
|||
combine_as_imports: options.combine_as_imports.unwrap_or(false),
|
||||
force_wrap_aliases: options.force_wrap_aliases.unwrap_or(false),
|
||||
split_on_trailing_comma: options.split_on_trailing_comma.unwrap_or(true),
|
||||
force_single_line: options.force_single_line.unwrap_or(false),
|
||||
single_line_exclusions: BTreeSet::from_iter(
|
||||
options.single_line_exclusions.unwrap_or_default(),
|
||||
),
|
||||
known_first_party: BTreeSet::from_iter(options.known_first_party.unwrap_or_default()),
|
||||
known_third_party: BTreeSet::from_iter(options.known_third_party.unwrap_or_default()),
|
||||
extra_standard_library: BTreeSet::from_iter(
|
||||
|
|
@ -125,6 +148,8 @@ impl Default for Settings {
|
|||
combine_as_imports: false,
|
||||
force_wrap_aliases: false,
|
||||
split_on_trailing_comma: true,
|
||||
force_single_line: false,
|
||||
single_line_exclusions: BTreeSet::new(),
|
||||
known_first_party: BTreeSet::new(),
|
||||
known_third_party: BTreeSet::new(),
|
||||
extra_standard_library: BTreeSet::new(),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
source: src/isort/mod.rs
|
||||
expression: checks
|
||||
---
|
||||
- kind: UnsortedImports
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 19
|
||||
column: 0
|
||||
fix:
|
||||
content: "import math\nimport sys\nfrom logging.handlers import FileHandler, StreamHandler\nfrom os import path, uname\n\n# comment 5\nfrom bar import a # comment 6\nfrom bar import b # comment 7\nfrom foo import bar # comment 3\nfrom foo2 import bar2 # comment 4\n\n# comment 1\n# comment 2\nfrom third_party import lib1\nfrom third_party import lib2\nfrom third_party import lib3\nfrom third_party import lib4\nfrom third_party import lib5\nfrom third_party import lib6\nfrom third_party import lib7\n"
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 19
|
||||
column: 0
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ impl Default for TrailingComma {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Hash, Ord, PartialOrd, Eq, PartialEq)]
|
||||
#[derive(Debug, Hash, Ord, PartialOrd, Eq, PartialEq, Clone)]
|
||||
pub struct ImportFromData<'a> {
|
||||
pub module: Option<&'a String>,
|
||||
pub level: Option<&'a usize>,
|
||||
|
|
@ -28,7 +28,7 @@ pub struct AliasData<'a> {
|
|||
pub asname: Option<&'a String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct CommentSet<'a> {
|
||||
pub atop: Vec<Cow<'a, str>>,
|
||||
pub inline: Vec<Cow<'a, str>>,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue