diff --git a/crates/ruff/resources/test/fixtures/isort/detect_same_package/foo/__init__.py b/crates/ruff/resources/test/fixtures/isort/detect_same_package/foo/__init__.py new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/crates/ruff/resources/test/fixtures/isort/detect_same_package/foo/bar.py b/crates/ruff/resources/test/fixtures/isort/detect_same_package/foo/bar.py new file mode 100644 index 00000000000000..b51f130ac15b1e --- /dev/null +++ b/crates/ruff/resources/test/fixtures/isort/detect_same_package/foo/bar.py @@ -0,0 +1,3 @@ +import os +import pandas +import foo.baz diff --git a/crates/ruff/resources/test/fixtures/isort/detect_same_package/pyproject.toml b/crates/ruff/resources/test/fixtures/isort/detect_same_package/pyproject.toml new file mode 100644 index 00000000000000..97fda9d62e0a92 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/isort/detect_same_package/pyproject.toml @@ -0,0 +1,2 @@ +[tool.ruff] +line-length = 88 diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index bc88a6e10b40fd..abb1bff8efb6f8 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -283,6 +283,7 @@ pub(crate) fn typing_only_runtime_import( None, &checker.settings.src, checker.package(), + checker.settings.isort.detect_same_package, &checker.settings.isort.known_modules, checker.settings.target_version, ) { diff --git a/crates/ruff/src/rules/isort/categorize.rs b/crates/ruff/src/rules/isort/categorize.rs index bc944ab96e5228..d4b86a8e0f5443 100644 --- a/crates/ruff/src/rules/isort/categorize.rs +++ b/crates/ruff/src/rules/isort/categorize.rs @@ -69,6 +69,7 @@ pub(crate) fn categorize<'a>( level: Option, src: &[PathBuf], package: Option<&Path>, + detect_same_package: bool, known_modules: &'a KnownModules, target_version: PythonVersion, ) -> &'a ImportSection { @@ -88,7 +89,7 @@ pub(crate) fn categorize<'a>( &ImportSection::Known(ImportType::StandardLibrary), Reason::KnownStandardLibrary, ) - } else if same_package(package, module_base) { + } else if detect_same_package && same_package(package, module_base) { ( &ImportSection::Known(ImportType::FirstParty), Reason::SamePackage, @@ -137,6 +138,7 @@ pub(crate) fn categorize_imports<'a>( block: ImportBlock<'a>, src: &[PathBuf], package: Option<&Path>, + detect_same_package: bool, known_modules: &'a KnownModules, target_version: PythonVersion, ) -> BTreeMap<&'a ImportSection, ImportBlock<'a>> { @@ -148,6 +150,7 @@ pub(crate) fn categorize_imports<'a>( None, src, package, + detect_same_package, known_modules, target_version, ); @@ -164,6 +167,7 @@ pub(crate) fn categorize_imports<'a>( import_from.level, src, package, + detect_same_package, known_modules, target_version, ); @@ -180,6 +184,7 @@ pub(crate) fn categorize_imports<'a>( import_from.level, src, package, + detect_same_package, known_modules, target_version, ); @@ -196,6 +201,7 @@ pub(crate) fn categorize_imports<'a>( import_from.level, src, package, + detect_same_package, known_modules, target_version, ); diff --git a/crates/ruff/src/rules/isort/mod.rs b/crates/ruff/src/rules/isort/mod.rs index c8780dd26d6c88..074f59be1530e2 100644 --- a/crates/ruff/src/rules/isort/mod.rs +++ b/crates/ruff/src/rules/isort/mod.rs @@ -82,6 +82,7 @@ pub(crate) fn format_imports( force_to_top: &BTreeSet, known_modules: &KnownModules, order_by_type: bool, + detect_same_package: bool, relative_imports_order: RelativeImportsOrder, single_line_exclusions: &BTreeSet, split_on_trailing_comma: bool, @@ -129,6 +130,7 @@ pub(crate) fn format_imports( force_to_top, known_modules, order_by_type, + detect_same_package, relative_imports_order, split_on_trailing_comma, classes, @@ -187,6 +189,7 @@ fn format_import_block( force_to_top: &BTreeSet, known_modules: &KnownModules, order_by_type: bool, + detect_same_package: bool, relative_imports_order: RelativeImportsOrder, split_on_trailing_comma: bool, classes: &BTreeSet, @@ -198,7 +201,14 @@ fn format_import_block( section_order: &[ImportSection], ) -> String { // Categorize by type (e.g., first-party vs. third-party). - let mut block_by_type = categorize_imports(block, src, package, known_modules, target_version); + let mut block_by_type = categorize_imports( + block, + src, + package, + detect_same_package, + known_modules, + target_version, + ); let mut output = String::new(); @@ -1084,4 +1094,38 @@ mod tests { assert_messages!(snapshot, diagnostics); Ok(()) } + + #[test] + fn detect_same_package() -> Result<()> { + let diagnostics = test_path( + Path::new("isort/detect_same_package/foo/bar.py"), + &Settings { + src: vec![], + isort: super::settings::Settings { + detect_same_package: true, + ..super::settings::Settings::default() + }, + ..Settings::for_rule(Rule::UnsortedImports) + }, + )?; + assert_messages!(diagnostics); + Ok(()) + } + + #[test] + fn no_detect_same_package() -> Result<()> { + let diagnostics = test_path( + Path::new("isort/detect_same_package/foo/bar.py"), + &Settings { + src: vec![], + isort: super::settings::Settings { + detect_same_package: false, + ..super::settings::Settings::default() + }, + ..Settings::for_rule(Rule::UnsortedImports) + }, + )?; + assert_messages!(diagnostics); + Ok(()) + } } diff --git a/crates/ruff/src/rules/isort/rules/organize_imports.rs b/crates/ruff/src/rules/isort/rules/organize_imports.rs index 62c7ba47d7e240..3681ba1af61ff9 100644 --- a/crates/ruff/src/rules/isort/rules/organize_imports.rs +++ b/crates/ruff/src/rules/isort/rules/organize_imports.rs @@ -134,6 +134,7 @@ pub(crate) fn organize_imports( &settings.isort.force_to_top, &settings.isort.known_modules, settings.isort.order_by_type, + settings.isort.detect_same_package, settings.isort.relative_imports_order, &settings.isort.single_line_exclusions, settings.isort.split_on_trailing_comma, diff --git a/crates/ruff/src/rules/isort/settings.rs b/crates/ruff/src/rules/isort/settings.rs index 8e9648df1bba23..310fcd74e48f8d 100644 --- a/crates/ruff/src/rules/isort/settings.rs +++ b/crates/ruff/src/rules/isort/settings.rs @@ -305,6 +305,21 @@ pub struct Options { )] /// Override in which order the sections should be output. Can be used to move custom sections. pub section_order: Option>, + #[option( + default = r#"true"#, + value_type = "bool", + example = r#" + detect-same-package = false + "# + )] + /// Whether to automatically mark imports from within the same package as first-party. + /// For example, when `detect-same-package = true`, then when analyzing files within the + /// `foo` package, any imports from within the `foo` package will be considered first-party. + /// + /// This heuristic is often unnecessary when `src` is configured to detect all first-party + /// sources; however, if `src` is _not_ configured, this heuristic can be useful to detect + /// first-party imports from _within_ (but not _across_) first-party packages. + pub detect_same_package: Option, // Tables are required to go last. #[option( default = "{}", @@ -331,6 +346,7 @@ pub struct Settings { pub force_wrap_aliases: bool, pub force_to_top: BTreeSet, pub known_modules: KnownModules, + pub detect_same_package: bool, pub order_by_type: bool, pub relative_imports_order: RelativeImportsOrder, pub single_line_exclusions: BTreeSet, @@ -352,6 +368,7 @@ impl Default for Settings { combine_as_imports: false, force_single_line: false, force_sort_within_sections: false, + detect_same_package: true, case_sensitive: false, force_wrap_aliases: false, force_to_top: BTreeSet::new(), @@ -509,6 +526,7 @@ impl TryFrom for Settings { force_sort_within_sections: options.force_sort_within_sections.unwrap_or(false), case_sensitive: options.case_sensitive.unwrap_or(false), force_wrap_aliases: options.force_wrap_aliases.unwrap_or(false), + detect_same_package: options.detect_same_package.unwrap_or(true), force_to_top: BTreeSet::from_iter(options.force_to_top.unwrap_or_default()), known_modules: KnownModules::new( known_first_party, @@ -595,6 +613,7 @@ impl From for Options { force_sort_within_sections: Some(settings.force_sort_within_sections), case_sensitive: Some(settings.case_sensitive), force_wrap_aliases: Some(settings.force_wrap_aliases), + detect_same_package: Some(settings.detect_same_package), force_to_top: Some(settings.force_to_top.into_iter().collect()), known_first_party: Some( settings diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__detect_same_package.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__detect_same_package.snap new file mode 100644 index 00000000000000..4b75e7ede697ab --- /dev/null +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__detect_same_package.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff/src/rules/isort/mod.rs +--- +bar.py:1:1: I001 [*] Import block is un-sorted or un-formatted + | +1 | / import os +2 | | import pandas +3 | | import foo.baz + | + = help: Organize imports + +ℹ Fix +1 1 | import os + 2 |+ +2 3 | import pandas + 4 |+ +3 5 | import foo.baz + + diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__detect_same_package_foo__bar.py.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__detect_same_package_foo__bar.py.snap new file mode 100644 index 00000000000000..4b75e7ede697ab --- /dev/null +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__detect_same_package_foo__bar.py.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff/src/rules/isort/mod.rs +--- +bar.py:1:1: I001 [*] Import block is un-sorted or un-formatted + | +1 | / import os +2 | | import pandas +3 | | import foo.baz + | + = help: Organize imports + +ℹ Fix +1 1 | import os + 2 |+ +2 3 | import pandas + 4 |+ +3 5 | import foo.baz + + diff --git a/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__no_detect_same_package.snap b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__no_detect_same_package.snap new file mode 100644 index 00000000000000..2036ce6762b82a --- /dev/null +++ b/crates/ruff/src/rules/isort/snapshots/ruff__rules__isort__tests__no_detect_same_package.snap @@ -0,0 +1,19 @@ +--- +source: crates/ruff/src/rules/isort/mod.rs +--- +bar.py:1:1: I001 [*] Import block is un-sorted or un-formatted + | +1 | / import os +2 | | import pandas +3 | | import foo.baz + | + = help: Organize imports + +ℹ Fix +1 1 | import os +2 |-import pandas + 2 |+ +3 3 | import foo.baz + 4 |+import pandas + + diff --git a/ruff.schema.json b/ruff.schema.json index c1591cb938aa41..beb32b04ede191 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1189,6 +1189,13 @@ "type": "string" } }, + "detect-same-package": { + "description": "Whether to automatically mark imports from within the same package as first-party. For example, when `detect-same-package = true`, then when analyzing files within the `foo` package, any imports from within the `foo` package will be considered first-party.\n\nThis heuristic is often unnecessary when `src` is configured to detect all first-party sources; however, if `src` is _not_ configured, this heuristic can be useful to detect first-party imports from _within_ (but not _across_) first-party packages.", + "type": [ + "boolean", + "null" + ] + }, "extra-standard-library": { "description": "A list of modules to consider standard-library, in addition to those known to Ruff in advance.\n\nSupports glob patterns. For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).", "type": [