Implement isort custom sections and ordering (#2419) (#3900)

This commit is contained in:
Paul 2023-04-13 23:28:22 +02:00 committed by GitHub
parent 1f22e035e3
commit 2d2630ef07
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 400 additions and 94 deletions

View file

@ -0,0 +1,7 @@
from __future__ import annotations
import os
import sys
import pytz
import django.settings
from library import foo
from . import local

View file

@ -6,7 +6,7 @@ use ruff_python_semantic::binding::{
Binding, BindingKind, ExecutionContext, FromImportation, Importation, SubmoduleImportation,
};
use crate::rules::isort::{categorize, ImportType};
use crate::rules::isort::{categorize, ImportSection, ImportType};
use crate::settings::Settings;
/// ## What it does
@ -294,25 +294,31 @@ pub fn typing_only_runtime_import(
&settings.isort.known_modules,
settings.target_version,
) {
ImportType::LocalFolder | ImportType::FirstParty => Some(Diagnostic::new(
TypingOnlyFirstPartyImport {
full_name: full_name.to_string(),
},
binding.range,
)),
ImportType::ThirdParty => Some(Diagnostic::new(
TypingOnlyThirdPartyImport {
full_name: full_name.to_string(),
},
binding.range,
)),
ImportType::StandardLibrary => Some(Diagnostic::new(
ImportSection::Known(ImportType::LocalFolder | ImportType::FirstParty) => {
Some(Diagnostic::new(
TypingOnlyFirstPartyImport {
full_name: full_name.to_string(),
},
binding.range,
))
}
ImportSection::Known(ImportType::ThirdParty) | ImportSection::UserDefined(_) => {
Some(Diagnostic::new(
TypingOnlyThirdPartyImport {
full_name: full_name.to_string(),
},
binding.range,
))
}
ImportSection::Known(ImportType::StandardLibrary) => Some(Diagnostic::new(
TypingOnlyStandardLibraryImport {
full_name: full_name.to_string(),
},
binding.range,
)),
ImportType::Future => unreachable!("`__future__` imports should be marked as used"),
ImportSection::Known(ImportType::Future) => {
unreachable!("`__future__` imports should be marked as used")
}
}
} else {
None

View file

@ -1,8 +1,9 @@
use std::collections::{BTreeMap, BTreeSet};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::{fs, iter};
use log::debug;
use rustc_hash::FxHashMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use strum_macros::EnumIter;
@ -11,6 +12,7 @@ use ruff_macros::CacheKey;
use ruff_python_stdlib::sys::KNOWN_STANDARD_LIBRARY;
use crate::settings::types::PythonVersion;
use crate::warn_user_once;
use super::types::{ImportBlock, Importable};
@ -37,6 +39,15 @@ pub enum ImportType {
LocalFolder,
}
#[derive(
Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema, CacheKey,
)]
#[serde(untagged)]
pub enum ImportSection {
Known(ImportType),
UserDefined(String),
}
#[derive(Debug)]
enum Reason<'a> {
NonZeroLevel,
@ -49,23 +60,27 @@ enum Reason<'a> {
SamePackage,
SourceMatch(&'a Path),
NoMatch,
UserDefinedSection,
}
#[allow(clippy::too_many_arguments)]
pub fn categorize(
pub fn categorize<'a>(
module_name: &str,
level: Option<usize>,
src: &[PathBuf],
package: Option<&Path>,
known_modules: &KnownModules,
known_modules: &'a KnownModules,
target_version: PythonVersion,
) -> ImportType {
) -> &'a ImportSection {
let module_base = module_name.split('.').next().unwrap();
let (import_type, reason) = {
if level.map_or(false, |level| level > 0) {
(ImportType::LocalFolder, Reason::NonZeroLevel)
(
&ImportSection::Known(ImportType::LocalFolder),
Reason::NonZeroLevel,
)
} else if module_base == "__future__" {
(ImportType::Future, Reason::Future)
(&ImportSection::Known(ImportType::Future), Reason::Future)
} else if let Some((import_type, reason)) = known_modules.categorize(module_name) {
(import_type, reason)
} else if KNOWN_STANDARD_LIBRARY
@ -73,13 +88,25 @@ pub fn categorize(
.unwrap()
.contains(module_base)
{
(ImportType::StandardLibrary, Reason::KnownStandardLibrary)
(
&ImportSection::Known(ImportType::StandardLibrary),
Reason::KnownStandardLibrary,
)
} else if same_package(package, module_base) {
(ImportType::FirstParty, Reason::SamePackage)
(
&ImportSection::Known(ImportType::FirstParty),
Reason::SamePackage,
)
} else if let Some(src) = match_sources(src, module_base) {
(ImportType::FirstParty, Reason::SourceMatch(src))
(
&ImportSection::Known(ImportType::FirstParty),
Reason::SourceMatch(src),
)
} else {
(ImportType::ThirdParty, Reason::NoMatch)
(
&ImportSection::Known(ImportType::ThirdParty),
Reason::NoMatch,
)
}
};
debug!(
@ -114,10 +141,10 @@ pub fn categorize_imports<'a>(
block: ImportBlock<'a>,
src: &[PathBuf],
package: Option<&Path>,
known_modules: &KnownModules,
known_modules: &'a KnownModules,
target_version: PythonVersion,
) -> BTreeMap<ImportType, ImportBlock<'a>> {
let mut block_by_type: BTreeMap<ImportType, ImportBlock> = BTreeMap::default();
) -> BTreeMap<&'a ImportSection, ImportBlock<'a>> {
let mut block_by_type: BTreeMap<&ImportSection, ImportBlock> = BTreeMap::default();
// Categorize `StmtKind::Import`.
for (alias, comments) in block.import {
let import_type = categorize(
@ -188,13 +215,17 @@ pub fn categorize_imports<'a>(
#[derive(Debug, Default, CacheKey)]
pub struct KnownModules {
/// A set of user-provided first-party modules.
pub first_party: BTreeSet<String>,
pub first_party: Vec<String>,
/// A set of user-provided third-party modules.
pub third_party: BTreeSet<String>,
pub third_party: Vec<String>,
/// A set of user-provided local folder modules.
pub local_folder: BTreeSet<String>,
pub local_folder: Vec<String>,
/// A set of user-provided standard library modules.
pub standard_library: BTreeSet<String>,
pub standard_library: Vec<String>,
/// A map of additional user-provided sections.
pub user_defined: FxHashMap<String, Vec<String>>,
/// A map of known modules to their section.
pub known: FxHashMap<String, ImportSection>,
/// Whether any of the known modules are submodules (e.g., `foo.bar`, as opposed to `foo`).
has_submodules: bool,
}
@ -205,29 +236,67 @@ impl KnownModules {
third_party: Vec<String>,
local_folder: Vec<String>,
standard_library: Vec<String>,
user_defined: FxHashMap<String, Vec<String>>,
) -> Self {
let first_party = BTreeSet::from_iter(first_party);
let third_party = BTreeSet::from_iter(third_party);
let local_folder = BTreeSet::from_iter(local_folder);
let standard_library = BTreeSet::from_iter(standard_library);
let has_submodules = first_party
let modules = user_defined
.iter()
.chain(third_party.iter())
.chain(local_folder.iter())
.chain(standard_library.iter())
.any(|m| m.contains('.'));
.flat_map(|(section, modules)| {
modules
.iter()
.cloned()
.map(|module| (module, ImportSection::UserDefined(section.clone())))
})
.chain(
first_party
.iter()
.cloned()
.map(|module| (module, ImportSection::Known(ImportType::FirstParty))),
)
.chain(
third_party
.iter()
.cloned()
.map(|module| (module, ImportSection::Known(ImportType::ThirdParty))),
)
.chain(
local_folder
.iter()
.cloned()
.map(|module| (module, ImportSection::Known(ImportType::LocalFolder))),
)
.chain(
standard_library
.iter()
.cloned()
.map(|module| (module, ImportSection::Known(ImportType::StandardLibrary))),
);
let mut known = FxHashMap::with_capacity_and_hasher(
modules.size_hint().0,
std::hash::BuildHasherDefault::default(),
);
modules.for_each(|(module, section)| {
if known.insert(module, section).is_some() {
warn_user_once!("One or more modules are part of multiple import sections.");
}
});
let has_submodules = known.keys().any(|module| module.contains('.'));
Self {
first_party,
third_party,
local_folder,
standard_library,
user_defined,
known,
has_submodules,
}
}
/// Return the [`ImportType`] for a given module, if it's been categorized as a known module
/// Return the [`ImportSection`] for a given module, if it's been categorized as a known module
/// by the user.
fn categorize(&self, module_name: &str) -> Option<(ImportType, Reason)> {
fn categorize(&self, module_name: &str) -> Option<(&ImportSection, Reason)> {
if self.has_submodules {
// Check all module prefixes from the longest to the shortest (e.g., given
// `foo.bar.baz`, check `foo.bar.baz`, then `foo.bar`, then `foo`, taking the first,
@ -239,34 +308,33 @@ impl KnownModules {
.rev()
{
let submodule = &module_name[0..i];
if self.first_party.contains(submodule) {
return Some((ImportType::FirstParty, Reason::KnownFirstParty));
}
if self.third_party.contains(submodule) {
return Some((ImportType::ThirdParty, Reason::KnownThirdParty));
}
if self.local_folder.contains(submodule) {
return Some((ImportType::LocalFolder, Reason::KnownLocalFolder));
}
if self.standard_library.contains(submodule) {
return Some((ImportType::StandardLibrary, Reason::ExtraStandardLibrary));
if let Some(result) = self.categorize_submodule(submodule) {
return Some(result);
}
}
None
} else {
// Happy path: no submodules, so we can check the module base and be done.
let module_base = module_name.split('.').next().unwrap();
if self.first_party.contains(module_base) {
Some((ImportType::FirstParty, Reason::KnownFirstParty))
} else if self.third_party.contains(module_base) {
Some((ImportType::ThirdParty, Reason::KnownThirdParty))
} else if self.local_folder.contains(module_base) {
Some((ImportType::LocalFolder, Reason::KnownLocalFolder))
} else if self.standard_library.contains(module_base) {
Some((ImportType::StandardLibrary, Reason::ExtraStandardLibrary))
} else {
None
}
self.categorize_submodule(module_base)
}
}
fn categorize_submodule(&self, submodule: &str) -> Option<(&ImportSection, Reason)> {
if let Some(section) = self.known.get(submodule) {
let reason = match section {
ImportSection::UserDefined(_) => Reason::UserDefinedSection,
ImportSection::Known(ImportType::FirstParty) => Reason::KnownFirstParty,
ImportSection::Known(ImportType::ThirdParty) => Reason::KnownThirdParty,
ImportSection::Known(ImportType::LocalFolder) => Reason::KnownLocalFolder,
ImportSection::Known(ImportType::StandardLibrary) => Reason::ExtraStandardLibrary,
ImportSection::Known(ImportType::Future) => {
unreachable!("__future__ imports are not known")
}
};
Some((section, reason))
} else {
None
}
}
}

View file

@ -9,7 +9,7 @@ use strum::IntoEnumIterator;
use crate::rules::isort::categorize::KnownModules;
use annotate::annotate_imports;
use categorize::categorize_imports;
pub use categorize::{categorize, ImportType};
pub use categorize::{categorize, ImportSection, ImportType};
use comments::Comment;
use normalize::normalize_imports;
use order::order_imports;
@ -22,6 +22,7 @@ use types::{AliasData, CommentSet, EitherImport, OrderedImportBlock, TrailingCom
use crate::rules::isort::types::ImportBlock;
use crate::settings::types::PythonVersion;
use crate::warn_user_once;
mod annotate;
mod categorize;
@ -131,11 +132,12 @@ pub fn format_imports(
classes: &BTreeSet<String>,
constants: &BTreeSet<String>,
variables: &BTreeSet<String>,
no_lines_before: &BTreeSet<ImportType>,
no_lines_before: &BTreeSet<ImportSection>,
lines_after_imports: isize,
lines_between_types: usize,
forced_separate: &[String],
target_version: PythonVersion,
section_order: &[ImportSection],
) -> String {
let trailer = &block.trailer;
let block = annotate_imports(&block.imports, comments, locator, split_on_trailing_comma);
@ -143,6 +145,50 @@ pub fn format_imports(
// Normalize imports (i.e., deduplicate, aggregate `from` imports).
let block = normalize_imports(block, combine_as_imports, force_single_line);
// Make sure all sections (built-in and user-defined) are present in the section order.
let mut section_order: Vec<_> = section_order.to_vec();
for known_section in ImportType::iter().map(ImportSection::Known) {
if !section_order.contains(&known_section) {
section_order.push(known_section);
}
}
// Verify that all sections listed in `section-order` are defined in `sections`.
for user_defined in &section_order {
if let ImportSection::UserDefined(section_name) = user_defined {
if !known_modules.user_defined.contains_key(section_name) {
warn_user_once!(
"`section-order` contains unknown user-defined section: `{}`.",
section_name
);
}
}
}
// Verify that all sections listed in `no-lines-before` are defined in `sections`.
for user_defined in no_lines_before {
if let ImportSection::UserDefined(section_name) = user_defined {
if !known_modules.user_defined.contains_key(section_name) {
warn_user_once!(
"`no-lines-before` contains unknown user-defined section: `{}`.",
section_name
);
}
}
}
// Verify that all sections defined in `sections` are listed in `section-order`.
for section_name in known_modules.user_defined.keys() {
let section = ImportSection::UserDefined(section_name.clone());
if !section_order.contains(&section) {
warn_user_once!(
"`section-order` is missing user-defined section: `{}`.",
section_name
);
section_order.push(section);
}
}
let mut output = String::new();
for block in split::split_by_forced_separate(block, forced_separate) {
@ -167,6 +213,7 @@ pub fn format_imports(
no_lines_before,
lines_between_types,
target_version,
&section_order,
);
if !block_output.is_empty() && !output.is_empty() {
@ -221,9 +268,10 @@ fn format_import_block(
classes: &BTreeSet<String>,
constants: &BTreeSet<String>,
variables: &BTreeSet<String>,
no_lines_before: &BTreeSet<ImportType>,
no_lines_before: &BTreeSet<ImportSection>,
lines_between_types: usize,
target_version: PythonVersion,
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);
@ -233,10 +281,10 @@ fn format_import_block(
// Generate replacement source code.
let mut is_first_block = true;
let mut pending_lines_before = false;
for import_type in ImportType::iter() {
let import_block = block_by_type.remove(&import_type);
for import_section in section_order {
let import_block = block_by_type.remove(import_section);
if !no_lines_before.contains(&import_type) {
if !no_lines_before.contains(import_section) {
pending_lines_before = true;
}
let Some(import_block) = import_block else {
@ -327,6 +375,7 @@ fn format_import_block(
#[cfg(test)]
mod tests {
use rustc_hash::FxHashMap;
use std::collections::BTreeSet;
use std::path::Path;
@ -336,7 +385,7 @@ mod tests {
use test_case::test_case;
use crate::registry::Rule;
use crate::rules::isort::categorize::KnownModules;
use crate::rules::isort::categorize::{ImportSection, KnownModules};
use crate::settings::Settings;
use crate::test::{test_path, test_resource_path};
@ -413,6 +462,7 @@ mod tests {
vec!["foo".to_string(), "__future__".to_string()],
vec![],
vec![],
FxHashMap::default(),
),
..super::settings::Settings::default()
},
@ -436,6 +486,7 @@ mod tests {
vec!["foo.bar".to_string()],
vec![],
vec![],
FxHashMap::default(),
),
..super::settings::Settings::default()
},
@ -477,6 +528,7 @@ mod tests {
vec![],
vec!["ruff".to_string()],
vec![],
FxHashMap::default(),
),
..super::settings::Settings::default()
},
@ -831,11 +883,11 @@ mod tests {
&Settings {
isort: super::settings::Settings {
no_lines_before: BTreeSet::from([
ImportType::Future,
ImportType::StandardLibrary,
ImportType::ThirdParty,
ImportType::FirstParty,
ImportType::LocalFolder,
ImportSection::Known(ImportType::Future),
ImportSection::Known(ImportType::StandardLibrary),
ImportSection::Known(ImportType::ThirdParty),
ImportSection::Known(ImportType::FirstParty),
ImportSection::Known(ImportType::LocalFolder),
]),
..super::settings::Settings::default()
},
@ -859,8 +911,8 @@ mod tests {
&Settings {
isort: super::settings::Settings {
no_lines_before: BTreeSet::from([
ImportType::StandardLibrary,
ImportType::LocalFolder,
ImportSection::Known(ImportType::StandardLibrary),
ImportSection::Known(ImportType::LocalFolder),
]),
..super::settings::Settings::default()
},
@ -935,4 +987,60 @@ mod tests {
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test_case(Path::new("sections.py"))]
fn sections(path: &Path) -> Result<()> {
let snapshot = format!("sections_{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("isort").join(path).as_path(),
&Settings {
src: vec![test_resource_path("fixtures/isort")],
isort: super::settings::Settings {
known_modules: KnownModules::new(
vec![],
vec![],
vec![],
vec![],
FxHashMap::from_iter([("django".to_string(), vec!["django".to_string()])]),
),
..super::settings::Settings::default()
},
..Settings::for_rule(Rule::UnsortedImports)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test_case(Path::new("sections.py"))]
fn section_order(path: &Path) -> Result<()> {
let snapshot = format!("section_order_{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("isort").join(path).as_path(),
&Settings {
src: vec![test_resource_path("fixtures/isort")],
isort: super::settings::Settings {
known_modules: KnownModules::new(
vec!["library".to_string()],
vec![],
vec![],
vec![],
FxHashMap::from_iter([("django".to_string(), vec!["django".to_string()])]),
),
section_order: vec![
ImportSection::Known(ImportType::Future),
ImportSection::Known(ImportType::StandardLibrary),
ImportSection::Known(ImportType::ThirdParty),
ImportSection::UserDefined("django".to_string()),
ImportSection::Known(ImportType::FirstParty),
ImportSection::Known(ImportType::LocalFolder),
],
..super::settings::Settings::default()
},
..Settings::for_rule(Rule::UnsortedImports)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
}

View file

@ -139,6 +139,7 @@ pub fn organize_imports(
settings.isort.lines_between_types,
&settings.isort.forced_separate,
settings.target_version,
&settings.isort.section_order,
);
// Expand the span the entire range, including leading and trailing space.

View file

@ -1,5 +1,6 @@
//! Settings for the `isort` plugin.
use rustc_hash::FxHashMap;
use std::collections::BTreeSet;
use schemars::JsonSchema;
@ -8,7 +9,7 @@ use serde::{Deserialize, Serialize};
use crate::rules::isort::categorize::KnownModules;
use ruff_macros::{CacheKey, ConfigurationOptions};
use super::categorize::ImportType;
use super::categorize::ImportSection;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, CacheKey, JsonSchema)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
@ -226,14 +227,14 @@ pub struct Options {
pub variables: Option<Vec<String>>,
#[option(
default = r#"[]"#,
value_type = r#"list["future" | "standard-library" | "third-party" | "first-party" | "local-folder"]"#,
value_type = r#"list["future" | "standard-library" | "third-party" | "first-party" | "local-folder" | str]"#,
example = r#"
no-lines-before = ["future", "standard-library"]
"#
)]
/// A list of sections that should _not_ be delineated from the previous
/// section via empty lines.
pub no_lines_before: Option<Vec<ImportType>>,
pub no_lines_before: Option<Vec<ImportSection>>,
#[option(
default = r#"-1"#,
value_type = "int",
@ -265,6 +266,28 @@ pub struct Options {
/// A list of modules to separate into auxiliary block(s) of imports,
/// in the order specified.
pub forced_separate: Option<Vec<String>>,
#[option(
default = r#"[]"#,
value_type = r#"list["future" | "standard-library" | "third-party" | "first-party" | "local-folder" | str]"#,
example = r#"
section-order = ["future", "standard-library", "first-party", "local-folder", "third-party"]
"#
)]
/// Override in which order the sections should be output. Can be used to move custom sections.
pub section_order: Option<Vec<ImportSection>>,
// Tables are required to go last.
#[option(
default = "{}",
value_type = "dict[str, list[str]]",
example = r#"
# Group all Django imports into a separate section.
[tool.ruff.isort.sections]
"django" = ["django"]
"#
)]
/// A list of mappings from section names to modules.
/// By default custom sections are output last, but this can be overridden with `section-order`.
pub sections: Option<FxHashMap<String, Vec<String>>>,
}
#[derive(Debug, CacheKey)]
@ -284,10 +307,11 @@ pub struct Settings {
pub classes: BTreeSet<String>,
pub constants: BTreeSet<String>,
pub variables: BTreeSet<String>,
pub no_lines_before: BTreeSet<ImportType>,
pub no_lines_before: BTreeSet<ImportSection>,
pub lines_after_imports: isize,
pub lines_between_types: usize,
pub forced_separate: Vec<String>,
pub section_order: Vec<ImportSection>,
}
impl Default for Settings {
@ -311,6 +335,7 @@ impl Default for Settings {
lines_after_imports: -1,
lines_between_types: 0,
forced_separate: Vec::new(),
section_order: Vec::new(),
}
}
}
@ -329,6 +354,7 @@ impl From<Options> for Settings {
options.known_third_party.unwrap_or_default(),
options.known_local_folder.unwrap_or_default(),
options.extra_standard_library.unwrap_or_default(),
options.sections.unwrap_or_default(),
),
order_by_type: options.order_by_type.unwrap_or(true),
relative_imports_order: options.relative_imports_order.unwrap_or_default(),
@ -343,6 +369,7 @@ impl From<Options> for Settings {
lines_after_imports: options.lines_after_imports.unwrap_or(-1),
lines_between_types: options.lines_between_types.unwrap_or_default(),
forced_separate: Vec::from_iter(options.forced_separate.unwrap_or_default()),
section_order: Vec::from_iter(options.section_order.unwrap_or_default()),
}
}
}
@ -352,20 +379,14 @@ impl From<Settings> for Options {
Self {
required_imports: Some(settings.required_imports.into_iter().collect()),
combine_as_imports: Some(settings.combine_as_imports),
extra_standard_library: Some(
settings
.known_modules
.standard_library
.into_iter()
.collect(),
),
extra_standard_library: Some(settings.known_modules.standard_library),
force_single_line: Some(settings.force_single_line),
force_sort_within_sections: Some(settings.force_sort_within_sections),
force_wrap_aliases: Some(settings.force_wrap_aliases),
force_to_top: Some(settings.force_to_top.into_iter().collect()),
known_first_party: Some(settings.known_modules.first_party.into_iter().collect()),
known_third_party: Some(settings.known_modules.third_party.into_iter().collect()),
known_local_folder: Some(settings.known_modules.local_folder.into_iter().collect()),
known_first_party: Some(settings.known_modules.first_party),
known_third_party: Some(settings.known_modules.third_party),
known_local_folder: Some(settings.known_modules.local_folder),
order_by_type: Some(settings.order_by_type),
relative_imports_order: Some(settings.relative_imports_order),
single_line_exclusions: Some(settings.single_line_exclusions.into_iter().collect()),
@ -377,6 +398,8 @@ impl From<Settings> for Options {
lines_after_imports: Some(settings.lines_after_imports),
lines_between_types: Some(settings.lines_between_types),
forced_separate: Some(settings.forced_separate.into_iter().collect()),
section_order: Some(settings.section_order.into_iter().collect()),
sections: Some(settings.known_modules.user_defined),
}
}
}

View file

@ -0,0 +1,30 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---
sections.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / from __future__ import annotations
2 | | import os
3 | | import sys
4 | | import pytz
5 | | import django.settings
6 | | from library import foo
7 | | from . import local
|
= help: Organize imports
Suggested fix
1 1 | from __future__ import annotations
2 |+
2 3 | import os
3 4 | import sys
5 |+
4 6 | import pytz
7 |+
5 8 | import django.settings
9 |+
6 10 | from library import foo
11 |+
7 12 | from . import local

View file

@ -0,0 +1,30 @@
---
source: crates/ruff/src/rules/isort/mod.rs
---
sections.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / from __future__ import annotations
2 | | import os
3 | | import sys
4 | | import pytz
5 | | import django.settings
6 | | from library import foo
7 | | from . import local
|
= help: Organize imports
Suggested fix
1 1 | from __future__ import annotations
2 |+
2 3 | import os
3 4 | import sys
5 |+
4 6 | import pytz
5 |-import django.settings
6 7 | from library import foo
8 |+
7 9 | from . import local
10 |+
11 |+import django.settings

35
ruff.schema.json generated
View file

@ -1019,6 +1019,16 @@
},
"additionalProperties": false
},
"ImportSection": {
"anyOf": [
{
"$ref": "#/definitions/ImportType"
},
{
"type": "string"
}
]
},
"ImportType": {
"type": "string",
"enum": [
@ -1164,7 +1174,7 @@
"null"
],
"items": {
"$ref": "#/definitions/ImportType"
"$ref": "#/definitions/ImportSection"
}
},
"order-by-type": {
@ -1195,6 +1205,29 @@
"type": "string"
}
},
"section-order": {
"description": "Override in which order the sections should be output. Can be used to move custom sections.",
"type": [
"array",
"null"
],
"items": {
"$ref": "#/definitions/ImportSection"
}
},
"sections": {
"description": "A list of mappings from section names to modules. By default custom sections are output last, but this can be overridden with `section-order`.",
"type": [
"object",
"null"
],
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
},
"single-line-exclusions": {
"description": "One or more modules to exclude from the single line rule.",
"type": [