Skip to content

Commit 7ad07c2

Browse files
authored
Add allow-unused-imports setting for unused-import rule (F401) (#13601)
## Summary Resolves #9962 by allowing a configuration setting `allowed-unused-imports` TODO: - [x] Figure out the correct name and place for the setting; currently, I have added it top level. - [x] The comparison is pretty naive. I tried using `glob::Pattern` but couldn't get it to work in the configuration. - [x] Add tests - [x] Update documentations ## Test Plan `cargo test`
1 parent 4aefe52 commit 7ad07c2

File tree

9 files changed

+132
-4
lines changed

9 files changed

+132
-4
lines changed

crates/ruff/tests/snapshots/show_settings__display_default_settings.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ linter.pycodestyle.max_line_length = 88
359359
linter.pycodestyle.max_doc_length = none
360360
linter.pycodestyle.ignore_overlong_task_comments = false
361361
linter.pyflakes.extend_generics = []
362+
linter.pyflakes.allowed_unused_imports = []
362363
linter.pylint.allow_magic_value_types = [
363364
str,
364365
bytes,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""
2+
Test: allowed-unused-imports
3+
"""
4+
5+
# OK
6+
import hvplot.pandas
7+
import hvplot.pandas.plots
8+
from hvplot.pandas import scatter_matrix
9+
from hvplot.pandas.plots import scatter_matrix
10+
11+
# Errors
12+
from hvplot.pandas_alias import scatter_matrix

crates/ruff_linter/src/rules/pyflakes/mod.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,21 @@ mod tests {
327327
assert_messages!(snapshot, diagnostics);
328328
Ok(())
329329
}
330+
#[test_case(Rule::UnusedImport, Path::new("F401_31.py"))]
331+
fn f401_allowed_unused_imports_option(rule_code: Rule, path: &Path) -> Result<()> {
332+
let diagnostics = test_path(
333+
Path::new("pyflakes").join(path).as_path(),
334+
&LinterSettings {
335+
pyflakes: pyflakes::settings::Settings {
336+
allowed_unused_imports: vec!["hvplot.pandas".to_string()],
337+
..pyflakes::settings::Settings::default()
338+
},
339+
..LinterSettings::for_rule(rule_code)
340+
},
341+
)?;
342+
assert_messages!(diagnostics);
343+
Ok(())
344+
}
330345

331346
#[test]
332347
fn f841_dummy_variable_rgx() -> Result<()> {
@@ -427,7 +442,7 @@ mod tests {
427442
Path::new("pyflakes/project/foo/bar.py"),
428443
&LinterSettings {
429444
typing_modules: vec!["foo.typical".to_string()],
430-
..LinterSettings::for_rules(vec![Rule::UndefinedName])
445+
..LinterSettings::for_rule(Rule::UndefinedName)
431446
},
432447
)?;
433448
assert_messages!(diagnostics);
@@ -440,7 +455,7 @@ mod tests {
440455
Path::new("pyflakes/project/foo/bop/baz.py"),
441456
&LinterSettings {
442457
typing_modules: vec!["foo.typical".to_string()],
443-
..LinterSettings::for_rules(vec![Rule::UndefinedName])
458+
..LinterSettings::for_rule(Rule::UndefinedName)
444459
},
445460
)?;
446461
assert_messages!(diagnostics);
@@ -455,8 +470,9 @@ mod tests {
455470
&LinterSettings {
456471
pyflakes: pyflakes::settings::Settings {
457472
extend_generics: vec!["django.db.models.ForeignKey".to_string()],
473+
..pyflakes::settings::Settings::default()
458474
},
459-
..LinterSettings::for_rules(vec![Rule::UnusedImport])
475+
..LinterSettings::for_rule(Rule::UnusedImport)
460476
},
461477
)?;
462478
assert_messages!(snapshot, diagnostics);

crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use std::collections::BTreeMap;
66

77
use ruff_diagnostics::{Applicability, Diagnostic, Fix, FixAvailability, Violation};
88
use ruff_macros::{derive_message_formats, violation};
9+
use ruff_python_ast::name::QualifiedName;
910
use ruff_python_ast::{self as ast, Stmt};
1011
use ruff_python_semantic::{
1112
AnyImport, BindingKind, Exceptions, Imported, NodeId, Scope, SemanticModel, SubmoduleImport,
@@ -308,6 +309,20 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut
308309
continue;
309310
}
310311

312+
// If an import was marked as allowed, avoid treating it as unused.
313+
if checker
314+
.settings
315+
.pyflakes
316+
.allowed_unused_imports
317+
.iter()
318+
.any(|allowed_unused_import| {
319+
let allowed_unused_import = QualifiedName::from_dotted_name(allowed_unused_import);
320+
import.qualified_name().starts_with(&allowed_unused_import)
321+
})
322+
{
323+
continue;
324+
}
325+
311326
let import = ImportBinding {
312327
name,
313328
import,

crates/ruff_linter/src/rules/pyflakes/settings.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::fmt;
77
#[derive(Debug, Clone, Default, CacheKey)]
88
pub struct Settings {
99
pub extend_generics: Vec<String>,
10+
pub allowed_unused_imports: Vec<String>,
1011
}
1112

1213
impl fmt::Display for Settings {
@@ -15,7 +16,8 @@ impl fmt::Display for Settings {
1516
formatter = f,
1617
namespace = "linter.pyflakes",
1718
fields = [
18-
self.extend_generics | debug
19+
self.extend_generics | debug,
20+
self.allowed_unused_imports | debug
1921
]
2022
}
2123
Ok(())
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
3+
---
4+
F401_31.py:12:33: F401 [*] `hvplot.pandas_alias.scatter_matrix` imported but unused
5+
|
6+
11 | # Errors
7+
12 | from hvplot.pandas_alias import scatter_matrix
8+
| ^^^^^^^^^^^^^^ F401
9+
|
10+
= help: Remove unused import: `hvplot.pandas_alias.scatter_matrix`
11+
12+
Safe fix
13+
9 9 | from hvplot.pandas.plots import scatter_matrix
14+
10 10 |
15+
11 11 | # Errors
16+
12 |-from hvplot.pandas_alias import scatter_matrix

crates/ruff_workspace/src/configuration.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,7 @@ pub struct LintConfiguration {
626626
pub logger_objects: Option<Vec<String>>,
627627
pub task_tags: Option<Vec<String>>,
628628
pub typing_modules: Option<Vec<String>>,
629+
pub allowed_unused_imports: Option<Vec<String>>,
629630

630631
// Plugins
631632
pub flake8_annotations: Option<Flake8AnnotationsOptions>,
@@ -738,6 +739,7 @@ impl LintConfiguration {
738739
task_tags: options.common.task_tags,
739740
logger_objects: options.common.logger_objects,
740741
typing_modules: options.common.typing_modules,
742+
allowed_unused_imports: options.common.allowed_unused_imports,
741743
// Plugins
742744
flake8_annotations: options.common.flake8_annotations,
743745
flake8_bandit: options.common.flake8_bandit,
@@ -1106,6 +1108,9 @@ impl LintConfiguration {
11061108
.or(config.explicit_preview_rules),
11071109
task_tags: self.task_tags.or(config.task_tags),
11081110
typing_modules: self.typing_modules.or(config.typing_modules),
1111+
allowed_unused_imports: self
1112+
.allowed_unused_imports
1113+
.or(config.allowed_unused_imports),
11091114
// Plugins
11101115
flake8_annotations: self.flake8_annotations.combine(config.flake8_annotations),
11111116
flake8_bandit: self.flake8_bandit.combine(config.flake8_bandit),
@@ -1327,6 +1332,7 @@ fn warn_about_deprecated_top_level_lint_options(
13271332
explicit_preview_rules,
13281333
task_tags,
13291334
typing_modules,
1335+
allowed_unused_imports,
13301336
unfixable,
13311337
flake8_annotations,
13321338
flake8_bandit,
@@ -1425,6 +1431,9 @@ fn warn_about_deprecated_top_level_lint_options(
14251431
if typing_modules.is_some() {
14261432
used_options.push("typing-modules");
14271433
}
1434+
if allowed_unused_imports.is_some() {
1435+
used_options.push("allowed-unused-imports");
1436+
}
14281437

14291438
if unfixable.is_some() {
14301439
used_options.push("unfixable");

crates/ruff_workspace/src/options.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,16 @@ pub struct LintCommonOptions {
796796
)]
797797
pub typing_modules: Option<Vec<String>>,
798798

799+
/// A list of modules which is allowed even thought they are not used
800+
/// in the code.
801+
///
802+
/// This is useful when a module has a side effect when imported.
803+
#[option(
804+
default = r#"[]"#,
805+
value_type = "list[str]",
806+
example = r#"allowed-unused-imports = ["hvplot.pandas"]"#
807+
)]
808+
pub allowed_unused_imports: Option<Vec<String>>,
799809
/// A list of rule codes or prefixes to consider non-fixable.
800810
#[option(
801811
default = "[]",
@@ -2812,12 +2822,28 @@ pub struct PyflakesOptions {
28122822
example = "extend-generics = [\"django.db.models.ForeignKey\"]"
28132823
)]
28142824
pub extend_generics: Option<Vec<String>>,
2825+
2826+
/// A list of modules to ignore when considering unused imports.
2827+
///
2828+
/// Used to prevent violations for specific modules that are known to have side effects on
2829+
/// import (e.g., `hvplot.pandas`).
2830+
///
2831+
/// Modules in this list are expected to be fully-qualified names (e.g., `hvplot.pandas`). Any
2832+
/// submodule of a given module will also be ignored (e.g., given `hvplot`, `hvplot.pandas`
2833+
/// will also be ignored).
2834+
#[option(
2835+
default = r#"[]"#,
2836+
value_type = "list[str]",
2837+
example = r#"allowed-unused-imports = ["hvplot.pandas"]"#
2838+
)]
2839+
pub allowed_unused_imports: Option<Vec<String>>,
28152840
}
28162841

28172842
impl PyflakesOptions {
28182843
pub fn into_settings(self) -> pyflakes::settings::Settings {
28192844
pyflakes::settings::Settings {
28202845
extend_generics: self.extend_generics.unwrap_or_default(),
2846+
allowed_unused_imports: self.allowed_unused_imports.unwrap_or_default(),
28212847
}
28222848
}
28232849
}

ruff.schema.json

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)