Skip to content

Commit

Permalink
Add allow-unused-imports setting for unused-import rule (F401) (#…
Browse files Browse the repository at this point in the history
…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`
  • Loading branch information
hoxbro authored Oct 3, 2024
1 parent 4aefe52 commit 7ad07c2
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ linter.pycodestyle.max_line_length = 88
linter.pycodestyle.max_doc_length = none
linter.pycodestyle.ignore_overlong_task_comments = false
linter.pyflakes.extend_generics = []
linter.pyflakes.allowed_unused_imports = []
linter.pylint.allow_magic_value_types = [
str,
bytes,
Expand Down
12 changes: 12 additions & 0 deletions crates/ruff_linter/resources/test/fixtures/pyflakes/F401_31.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"""
Test: allowed-unused-imports
"""

# OK
import hvplot.pandas
import hvplot.pandas.plots
from hvplot.pandas import scatter_matrix
from hvplot.pandas.plots import scatter_matrix

# Errors
from hvplot.pandas_alias import scatter_matrix
22 changes: 19 additions & 3 deletions crates/ruff_linter/src/rules/pyflakes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,21 @@ mod tests {
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::UnusedImport, Path::new("F401_31.py"))]
fn f401_allowed_unused_imports_option(rule_code: Rule, path: &Path) -> Result<()> {
let diagnostics = test_path(
Path::new("pyflakes").join(path).as_path(),
&LinterSettings {
pyflakes: pyflakes::settings::Settings {
allowed_unused_imports: vec!["hvplot.pandas".to_string()],
..pyflakes::settings::Settings::default()
},
..LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(diagnostics);
Ok(())
}

#[test]
fn f841_dummy_variable_rgx() -> Result<()> {
Expand Down Expand Up @@ -427,7 +442,7 @@ mod tests {
Path::new("pyflakes/project/foo/bar.py"),
&LinterSettings {
typing_modules: vec!["foo.typical".to_string()],
..LinterSettings::for_rules(vec![Rule::UndefinedName])
..LinterSettings::for_rule(Rule::UndefinedName)
},
)?;
assert_messages!(diagnostics);
Expand All @@ -440,7 +455,7 @@ mod tests {
Path::new("pyflakes/project/foo/bop/baz.py"),
&LinterSettings {
typing_modules: vec!["foo.typical".to_string()],
..LinterSettings::for_rules(vec![Rule::UndefinedName])
..LinterSettings::for_rule(Rule::UndefinedName)
},
)?;
assert_messages!(diagnostics);
Expand All @@ -455,8 +470,9 @@ mod tests {
&LinterSettings {
pyflakes: pyflakes::settings::Settings {
extend_generics: vec!["django.db.models.ForeignKey".to_string()],
..pyflakes::settings::Settings::default()
},
..LinterSettings::for_rules(vec![Rule::UnusedImport])
..LinterSettings::for_rule(Rule::UnusedImport)
},
)?;
assert_messages!(snapshot, diagnostics);
Expand Down
15 changes: 15 additions & 0 deletions crates/ruff_linter/src/rules/pyflakes/rules/unused_import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::collections::BTreeMap;

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

// If an import was marked as allowed, avoid treating it as unused.
if checker
.settings
.pyflakes
.allowed_unused_imports
.iter()
.any(|allowed_unused_import| {
let allowed_unused_import = QualifiedName::from_dotted_name(allowed_unused_import);
import.qualified_name().starts_with(&allowed_unused_import)
})
{
continue;
}

let import = ImportBinding {
name,
import,
Expand Down
4 changes: 3 additions & 1 deletion crates/ruff_linter/src/rules/pyflakes/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::fmt;
#[derive(Debug, Clone, Default, CacheKey)]
pub struct Settings {
pub extend_generics: Vec<String>,
pub allowed_unused_imports: Vec<String>,
}

impl fmt::Display for Settings {
Expand All @@ -15,7 +16,8 @@ impl fmt::Display for Settings {
formatter = f,
namespace = "linter.pyflakes",
fields = [
self.extend_generics | debug
self.extend_generics | debug,
self.allowed_unused_imports | debug
]
}
Ok(())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F401_31.py:12:33: F401 [*] `hvplot.pandas_alias.scatter_matrix` imported but unused
|
11 | # Errors
12 | from hvplot.pandas_alias import scatter_matrix
| ^^^^^^^^^^^^^^ F401
|
= help: Remove unused import: `hvplot.pandas_alias.scatter_matrix`

Safe fix
9 9 | from hvplot.pandas.plots import scatter_matrix
10 10 |
11 11 | # Errors
12 |-from hvplot.pandas_alias import scatter_matrix
9 changes: 9 additions & 0 deletions crates/ruff_workspace/src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,7 @@ pub struct LintConfiguration {
pub logger_objects: Option<Vec<String>>,
pub task_tags: Option<Vec<String>>,
pub typing_modules: Option<Vec<String>>,
pub allowed_unused_imports: Option<Vec<String>>,

// Plugins
pub flake8_annotations: Option<Flake8AnnotationsOptions>,
Expand Down Expand Up @@ -738,6 +739,7 @@ impl LintConfiguration {
task_tags: options.common.task_tags,
logger_objects: options.common.logger_objects,
typing_modules: options.common.typing_modules,
allowed_unused_imports: options.common.allowed_unused_imports,
// Plugins
flake8_annotations: options.common.flake8_annotations,
flake8_bandit: options.common.flake8_bandit,
Expand Down Expand Up @@ -1106,6 +1108,9 @@ impl LintConfiguration {
.or(config.explicit_preview_rules),
task_tags: self.task_tags.or(config.task_tags),
typing_modules: self.typing_modules.or(config.typing_modules),
allowed_unused_imports: self
.allowed_unused_imports
.or(config.allowed_unused_imports),
// Plugins
flake8_annotations: self.flake8_annotations.combine(config.flake8_annotations),
flake8_bandit: self.flake8_bandit.combine(config.flake8_bandit),
Expand Down Expand Up @@ -1327,6 +1332,7 @@ fn warn_about_deprecated_top_level_lint_options(
explicit_preview_rules,
task_tags,
typing_modules,
allowed_unused_imports,
unfixable,
flake8_annotations,
flake8_bandit,
Expand Down Expand Up @@ -1425,6 +1431,9 @@ fn warn_about_deprecated_top_level_lint_options(
if typing_modules.is_some() {
used_options.push("typing-modules");
}
if allowed_unused_imports.is_some() {
used_options.push("allowed-unused-imports");
}

if unfixable.is_some() {
used_options.push("unfixable");
Expand Down
26 changes: 26 additions & 0 deletions crates/ruff_workspace/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,16 @@ pub struct LintCommonOptions {
)]
pub typing_modules: Option<Vec<String>>,

/// A list of modules which is allowed even thought they are not used
/// in the code.
///
/// This is useful when a module has a side effect when imported.
#[option(
default = r#"[]"#,
value_type = "list[str]",
example = r#"allowed-unused-imports = ["hvplot.pandas"]"#
)]
pub allowed_unused_imports: Option<Vec<String>>,
/// A list of rule codes or prefixes to consider non-fixable.
#[option(
default = "[]",
Expand Down Expand Up @@ -2812,12 +2822,28 @@ pub struct PyflakesOptions {
example = "extend-generics = [\"django.db.models.ForeignKey\"]"
)]
pub extend_generics: Option<Vec<String>>,

/// A list of modules to ignore when considering unused imports.
///
/// Used to prevent violations for specific modules that are known to have side effects on
/// import (e.g., `hvplot.pandas`).
///
/// Modules in this list are expected to be fully-qualified names (e.g., `hvplot.pandas`). Any
/// submodule of a given module will also be ignored (e.g., given `hvplot`, `hvplot.pandas`
/// will also be ignored).
#[option(
default = r#"[]"#,
value_type = "list[str]",
example = r#"allowed-unused-imports = ["hvplot.pandas"]"#
)]
pub allowed_unused_imports: Option<Vec<String>>,
}

impl PyflakesOptions {
pub fn into_settings(self) -> pyflakes::settings::Settings {
pyflakes::settings::Settings {
extend_generics: self.extend_generics.unwrap_or_default(),
allowed_unused_imports: self.allowed_unused_imports.unwrap_or_default(),
}
}
}
Expand Down
31 changes: 31 additions & 0 deletions ruff.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 7ad07c2

Please sign in to comment.