diff --git a/README.md b/README.md index c79856a0a3..d5f7de17e3 100644 --- a/README.md +++ b/README.md @@ -1787,6 +1787,24 @@ ban-relative-imports = "all" ### `isort` +#### [`combine-as-imports`](combine-as-imports) + +Combines as imports on the same line. See isort's [`combine-as-imports`](https://pycqa.github.io/isort/docs/configuration/options.html#combine-as-imports) +option. + +**Default value**: `false` + +**Type**: `bool` + +**Example usage**: + +```toml +[tool.ruff.isort] +combine-as-imports = true +``` + +--- + #### [`known-first-party`](known-first-party) A list of modules to consider first-party, regardless of whether they can be identified as such diff --git a/resources/test/fixtures/isort/combine_as_imports.py b/resources/test/fixtures/isort/combine_as_imports.py new file mode 100644 index 0000000000..a5e06ee310 --- /dev/null +++ b/resources/test/fixtures/isort/combine_as_imports.py @@ -0,0 +1,4 @@ +from module import Class as C +from module import CONSTANT +from module import function +from module import function as f diff --git a/src/isort/mod.rs b/src/isort/mod.rs index d55ccc49a0..7e2955bbc0 100644 --- a/src/isort/mod.rs +++ b/src/isort/mod.rs @@ -145,7 +145,7 @@ fn annotate_imports<'a>( annotated } -fn normalize_imports(imports: Vec) -> ImportBlock { +fn normalize_imports(imports: Vec, combine_as_imports: bool) -> ImportBlock { let mut block = ImportBlock::default(); for import in imports { match import { @@ -191,7 +191,7 @@ fn normalize_imports(imports: Vec) -> ImportBlock { } => { // Associate the comments with the first alias (best effort). if let Some(alias) = names.first() { - if alias.asname.is_none() { + if alias.asname.is_none() || combine_as_imports { let entry = &mut block .import_from .entry(ImportFromData { module, level }) @@ -225,7 +225,7 @@ fn normalize_imports(imports: Vec) -> ImportBlock { // Create an entry for every alias. for alias in names { - if alias.asname.is_none() { + if alias.asname.is_none() || combine_as_imports { let entry = block .import_from .entry(ImportFromData { module, level }) @@ -397,6 +397,7 @@ fn sort_imports(block: ImportBlock) -> OrderedImportBlock { ordered } +#[allow(clippy::too_many_arguments)] pub fn format_imports( block: &[&Stmt], comments: Vec, @@ -405,11 +406,12 @@ pub fn format_imports( known_first_party: &BTreeSet, known_third_party: &BTreeSet, extra_standard_library: &BTreeSet, + combine_as_imports: bool, ) -> String { let block = annotate_imports(block, comments); // Normalize imports (i.e., deduplicate, aggregate `from` imports). - let block = normalize_imports(block); + let block = normalize_imports(block, combine_as_imports); // Categorize by type (e.g., first-party vs. third-party). let block_by_type = categorize_imports( @@ -466,9 +468,10 @@ mod tests { use crate::checks::CheckCode; use crate::linter::test_path; - use crate::Settings; + use crate::{isort, Settings}; #[test_case(Path::new("add_newline_before_comments.py"))] + #[test_case(Path::new("combine_as_imports.py"))] #[test_case(Path::new("combine_import_froms.py"))] #[test_case(Path::new("comments.py"))] #[test_case(Path::new("deduplicate_imports.py"))] @@ -490,7 +493,7 @@ mod tests { #[test_case(Path::new("sort_similar_imports.py"))] #[test_case(Path::new("trailing_suffix.py"))] #[test_case(Path::new("type_comments.py"))] - fn isort(path: &Path) -> Result<()> { + fn default(path: &Path) -> Result<()> { let snapshot = format!("{}", path.to_string_lossy()); let mut checks = test_path( Path::new("./resources/test/fixtures/isort") @@ -506,4 +509,26 @@ mod tests { insta::assert_yaml_snapshot!(snapshot, checks); Ok(()) } + + #[test_case(Path::new("combine_as_imports.py"))] + fn combine_as_imports(path: &Path) -> Result<()> { + let snapshot = format!("combine_as_imports_{}", path.to_string_lossy()); + let mut checks = test_path( + Path::new("./resources/test/fixtures/isort") + .join(path) + .as_path(), + &Settings { + isort: isort::settings::Settings { + combine_as_imports: true, + ..isort::settings::Settings::default() + }, + src: vec![Path::new("resources/test/fixtures/isort").to_path_buf()], + ..Settings::for_rule(CheckCode::I001) + }, + true, + )?; + checks.sort_by_key(|check| check.location); + insta::assert_yaml_snapshot!(snapshot, checks); + Ok(()) + } } diff --git a/src/isort/plugins.rs b/src/isort/plugins.rs index 91e7277ba6..98d1eb297e 100644 --- a/src/isort/plugins.rs +++ b/src/isort/plugins.rs @@ -60,6 +60,7 @@ pub fn check_imports( &settings.isort.known_first_party, &settings.isort.known_third_party, &settings.isort.extra_standard_library, + settings.isort.combine_as_imports, ); if has_leading_content || has_trailing_content { diff --git a/src/isort/settings.rs b/src/isort/settings.rs index ba5d2e1365..a214179673 100644 --- a/src/isort/settings.rs +++ b/src/isort/settings.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(deny_unknown_fields, rename_all = "kebab-case")] pub struct Options { + pub combine_as_imports: Option, pub known_first_party: Option>, pub known_third_party: Option>, pub extra_standard_library: Option>, @@ -14,6 +15,7 @@ pub struct Options { #[derive(Debug, Hash, Default)] pub struct Settings { + pub combine_as_imports: bool, pub known_first_party: BTreeSet, pub known_third_party: BTreeSet, pub extra_standard_library: BTreeSet, @@ -22,6 +24,7 @@ pub struct Settings { impl Settings { pub fn from_options(options: Options) -> Self { Self { + combine_as_imports: options.combine_as_imports.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( diff --git a/src/isort/snapshots/ruff__isort__tests__combine_as_imports.py.snap b/src/isort/snapshots/ruff__isort__tests__combine_as_imports.py.snap new file mode 100644 index 0000000000..4d205ad4a3 --- /dev/null +++ b/src/isort/snapshots/ruff__isort__tests__combine_as_imports.py.snap @@ -0,0 +1,20 @@ +--- +source: src/isort/mod.rs +expression: checks +--- +- kind: UnsortedImports + location: + row: 1 + column: 0 + end_location: + row: 5 + column: 0 + fix: + content: "from module import CONSTANT, function\nfrom module import Class as C\nfrom module import function as f\n" + location: + row: 1 + column: 0 + end_location: + row: 5 + column: 0 + diff --git a/src/isort/snapshots/ruff__isort__tests__combine_as_imports_combine_as_imports.py.snap b/src/isort/snapshots/ruff__isort__tests__combine_as_imports_combine_as_imports.py.snap new file mode 100644 index 0000000000..456515bdcd --- /dev/null +++ b/src/isort/snapshots/ruff__isort__tests__combine_as_imports_combine_as_imports.py.snap @@ -0,0 +1,20 @@ +--- +source: src/isort/mod.rs +expression: checks +--- +- kind: UnsortedImports + location: + row: 1 + column: 0 + end_location: + row: 5 + column: 0 + fix: + content: "from module import CONSTANT, Class as C, function, function as f\n" + location: + row: 1 + column: 0 + end_location: + row: 5 + column: 0 +