Support isort's force-single-line option (#1366)

This commit is contained in:
Reiner Gerecke 2022-12-27 14:51:32 +01:00 committed by GitHub
parent e692c4a2cc
commit 534d8d049c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 210 additions and 4 deletions

View file

@ -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

View file

@ -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',

View file

@ -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"]

View 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
)

View file

@ -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": [

View file

@ -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(())
}
}

View file

@ -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.

View file

@ -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(),

View file

@ -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

View file

@ -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>>,