diff --git a/README.md b/README.md index cd8e42f17a..654d65dcb9 100644 --- a/README.md +++ b/README.md @@ -2877,6 +2877,24 @@ ignore-variadic-names = true ### `isort` +#### [`classes`](#classes) + +An override list of tokens to always recognize as a Class for +`order-by-type` regardless of casing. + +**Default value**: `[]` + +**Type**: `Vec` + +**Example usage**: + +```toml +[tool.ruff.isort] +classes = ["SVC"] +``` + +--- + #### [`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) diff --git a/resources/test/fixtures/isort/order_by_type_with_custom_classes.py b/resources/test/fixtures/isort/order_by_type_with_custom_classes.py new file mode 100644 index 0000000000..ff7fd46b5a --- /dev/null +++ b/resources/test/fixtures/isort/order_by_type_with_custom_classes.py @@ -0,0 +1,4 @@ +from sklearn.svm import func, SVC, CONST, Klass +from subprocess import N_CLASS, PIPE, Popen, STDOUT +from module import CLASS, Class, CONSTANT, function, BASIC, Apple +from torch.nn import SELU, AClass, A_CONSTANT diff --git a/ruff.schema.json b/ruff.schema.json index ba4d8a73aa..847819bfd4 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -755,6 +755,16 @@ "IsortOptions": { "type": "object", "properties": { + "classes": { + "description": "An override list of tokens to always recognize as a Class for `order-by-type` regardless of casing.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, "combine-as-imports": { "description": "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.", "type": [ diff --git a/src/isort/mod.rs b/src/isort/mod.rs index f85459dc3c..404f8e33fb 100644 --- a/src/isort/mod.rs +++ b/src/isort/mod.rs @@ -383,11 +383,12 @@ fn categorize_imports<'a>( block_by_type } -fn order_imports( - block: ImportBlock, +fn order_imports<'a>( + block: ImportBlock<'a>, order_by_type: bool, relative_imports_order: RelatveImportsOrder, -) -> OrderedImportBlock { + classes: &'a BTreeSet, +) -> OrderedImportBlock<'a> { let mut ordered = OrderedImportBlock::default(); // Sort `StmtKind::Import`. @@ -466,7 +467,7 @@ fn order_imports( aliases .into_iter() .sorted_by(|(alias1, _), (alias2, _)| { - cmp_members(alias1, alias2, order_by_type) + cmp_members(alias1, alias2, order_by_type, classes) }) .collect::>(), ) @@ -479,7 +480,7 @@ fn order_imports( (None, Some(_)) => Ordering::Less, (Some(_), None) => Ordering::Greater, (Some((alias1, _)), Some((alias2, _))) => { - cmp_members(alias1, alias2, order_by_type) + cmp_members(alias1, alias2, order_by_type, classes) } }, ) @@ -556,6 +557,7 @@ pub fn format_imports( relative_imports_order: RelatveImportsOrder, single_line_exclusions: &BTreeSet, split_on_trailing_comma: bool, + classes: &BTreeSet, ) -> String { let trailer = &block.trailer; let block = annotate_imports(&block.imports, comments, locator, split_on_trailing_comma); @@ -578,7 +580,8 @@ pub fn format_imports( // Generate replacement source code. let mut is_first_block = true; for import_block in block_by_type.into_values() { - let mut imports = order_imports(import_block, order_by_type, relative_imports_order); + let mut imports = + order_imports(import_block, order_by_type, relative_imports_order, classes); if force_single_line { imports = force_single_line_imports(imports, single_line_exclusions); @@ -698,6 +701,7 @@ mod tests { #[test_case(Path::new("split.py"))] #[test_case(Path::new("trailing_suffix.py"))] #[test_case(Path::new("type_comments.py"))] + #[test_case(Path::new("order_by_type_with_custom_classes.py"))] fn default(path: &Path) -> Result<()> { let snapshot = format!("{}", path.to_string_lossy()); let diagnostics = test_path( @@ -818,6 +822,36 @@ mod tests { Ok(()) } + #[test_case(Path::new("order_by_type_with_custom_classes.py"))] + fn order_by_type_with_custom_classes(path: &Path) -> Result<()> { + let snapshot = format!( + "order_by_type_with_custom_classes_{}", + path.to_string_lossy() + ); + let mut diagnostics = test_path( + Path::new("./resources/test/fixtures/isort") + .join(path) + .as_path(), + &Settings { + isort: isort::settings::Settings { + order_by_type: true, + classes: BTreeSet::from([ + "SVC".to_string(), + "SELU".to_string(), + "N_CLASS".to_string(), + "CLASS".to_string(), + ]), + ..isort::settings::Settings::default() + }, + src: vec![Path::new("resources/test/fixtures/isort").to_path_buf()], + ..Settings::for_rule(RuleCode::I001) + }, + )?; + diagnostics.sort_by_key(|diagnostic| diagnostic.location); + insta::assert_yaml_snapshot!(snapshot, diagnostics); + Ok(()) + } + #[test_case(Path::new("force_sort_within_sections.py"))] fn force_sort_within_sections(path: &Path) -> Result<()> { let snapshot = format!("force_sort_within_sections_{}", path.to_string_lossy()); diff --git a/src/isort/rules/organize_imports.rs b/src/isort/rules/organize_imports.rs index 40aea58fee..33d3dcf6b6 100644 --- a/src/isort/rules/organize_imports.rs +++ b/src/isort/rules/organize_imports.rs @@ -84,6 +84,7 @@ pub fn organize_imports( settings.isort.relative_imports_order, &settings.isort.single_line_exclusions, settings.isort.split_on_trailing_comma, + &settings.isort.classes, ); // Expand the span the entire range, including leading and trailing space. diff --git a/src/isort/settings.rs b/src/isort/settings.rs index 5db436eb16..c1711b316a 100644 --- a/src/isort/settings.rs +++ b/src/isort/settings.rs @@ -171,6 +171,16 @@ pub struct Options { )] /// Add the specified import line to all files. pub required_imports: Option>, + #[option( + default = r#"[]"#, + value_type = "Vec", + example = r#" + classes = ["SVC"] + "# + )] + /// An override list of tokens to always recognize as a Class for + /// `order-by-type` regardless of casing. + pub classes: Option>, } #[derive(Debug, Hash)] @@ -188,6 +198,7 @@ pub struct Settings { pub relative_imports_order: RelatveImportsOrder, pub single_line_exclusions: BTreeSet, pub split_on_trailing_comma: bool, + pub classes: BTreeSet, } impl Default for Settings { @@ -205,6 +216,7 @@ impl Default for Settings { relative_imports_order: RelatveImportsOrder::default(), single_line_exclusions: BTreeSet::new(), split_on_trailing_comma: true, + classes: BTreeSet::new(), } } } @@ -228,6 +240,7 @@ impl From for Settings { options.single_line_exclusions.unwrap_or_default(), ), split_on_trailing_comma: options.split_on_trailing_comma.unwrap_or(true), + classes: BTreeSet::from_iter(options.classes.unwrap_or_default()), } } } @@ -247,6 +260,7 @@ impl From for Options { relative_imports_order: Some(settings.relative_imports_order), single_line_exclusions: Some(settings.single_line_exclusions.into_iter().collect()), split_on_trailing_comma: Some(settings.split_on_trailing_comma), + classes: Some(settings.classes.into_iter().collect()), } } } diff --git a/src/isort/snapshots/ruff__isort__tests__order_by_type_with_custom_classes.py.snap b/src/isort/snapshots/ruff__isort__tests__order_by_type_with_custom_classes.py.snap new file mode 100644 index 0000000000..a4edc48381 --- /dev/null +++ b/src/isort/snapshots/ruff__isort__tests__order_by_type_with_custom_classes.py.snap @@ -0,0 +1,22 @@ +--- +source: src/isort/mod.rs +expression: diagnostics +--- +- kind: + UnsortedImports: ~ + location: + row: 1 + column: 0 + end_location: + row: 5 + column: 0 + fix: + content: "from subprocess import N_CLASS, PIPE, STDOUT, Popen\n\nfrom module import BASIC, CLASS, CONSTANT, Apple, Class, function\nfrom sklearn.svm import CONST, SVC, Klass, func\nfrom torch.nn import A_CONSTANT, SELU, AClass\n" + location: + row: 1 + column: 0 + end_location: + row: 5 + column: 0 + parent: ~ + diff --git a/src/isort/snapshots/ruff__isort__tests__order_by_type_with_custom_classes_order_by_type_with_custom_classes.py.snap b/src/isort/snapshots/ruff__isort__tests__order_by_type_with_custom_classes_order_by_type_with_custom_classes.py.snap new file mode 100644 index 0000000000..be91ca84f2 --- /dev/null +++ b/src/isort/snapshots/ruff__isort__tests__order_by_type_with_custom_classes_order_by_type_with_custom_classes.py.snap @@ -0,0 +1,22 @@ +--- +source: src/isort/mod.rs +expression: diagnostics +--- +- kind: + UnsortedImports: ~ + location: + row: 1 + column: 0 + end_location: + row: 5 + column: 0 + fix: + content: "from subprocess import PIPE, STDOUT, N_CLASS, Popen\n\nfrom module import BASIC, CONSTANT, Apple, CLASS, Class, function\nfrom sklearn.svm import CONST, Klass, SVC, func\nfrom torch.nn import A_CONSTANT, AClass, SELU\n" + location: + row: 1 + column: 0 + end_location: + row: 5 + column: 0 + parent: ~ + diff --git a/src/isort/sorting.rs b/src/isort/sorting.rs index a8046d4d5e..3d0b5a1842 100644 --- a/src/isort/sorting.rs +++ b/src/isort/sorting.rs @@ -1,5 +1,6 @@ /// See: use std::cmp::Ordering; +use std::collections::BTreeSet; use crate::isort::settings::RelatveImportsOrder; use crate::isort::types::EitherImport::{Import, ImportFrom}; @@ -13,8 +14,11 @@ pub enum Prefix { Variables, } -fn prefix(name: &str) -> Prefix { - if name.len() > 1 && string::is_upper(name) { +fn prefix(name: &str, classes: &BTreeSet) -> Prefix { + if classes.contains(name) { + // Ex) `CLASS` + Prefix::Classes + } else if name.len() > 1 && string::is_upper(name) { // Ex) `CONSTANT` Prefix::Constants } else if name.chars().next().map_or(false, char::is_uppercase) { @@ -39,10 +43,15 @@ pub fn cmp_modules(alias1: &AliasData, alias2: &AliasData) -> Ordering { } /// Compare two member imports within `StmtKind::ImportFrom` blocks. -pub fn cmp_members(alias1: &AliasData, alias2: &AliasData, order_by_type: bool) -> Ordering { +pub fn cmp_members( + alias1: &AliasData, + alias2: &AliasData, + order_by_type: bool, + classes: &BTreeSet, +) -> Ordering { if order_by_type { - prefix(alias1.name) - .cmp(&prefix(alias2.name)) + prefix(alias1.name, classes) + .cmp(&prefix(alias2.name, classes)) .then_with(|| cmp_modules(alias1, alias2)) } else { cmp_modules(alias1, alias2)