Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pyflakes.extend-generics setting #4677

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions crates/ruff/resources/test/fixtures/pyflakes/F401_15.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from typing import TYPE_CHECKING
from django.db.models import ForeignKey

if TYPE_CHECKING:
from pathlib import Path


class Foo:
var = ForeignKey["Path"]()
1 change: 1 addition & 0 deletions crates/ruff/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3866,6 +3866,7 @@ where
value,
&self.semantic_model,
self.settings.typing_modules.iter().map(String::as_str),
&self.settings.pyflakes.extend_generics,
) {
Some(subscript) => {
match subscript {
Expand Down
20 changes: 19 additions & 1 deletion crates/ruff/src/rules/pyflakes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub(crate) mod cformat;
pub(crate) mod fixes;
pub(crate) mod format;
pub(crate) mod rules;
pub mod settings;

#[cfg(test)]
mod tests {
Expand All @@ -19,7 +20,7 @@ mod tests {

use crate::linter::{check_path, LinterResult};
use crate::registry::{AsRule, Linter, Rule};
use crate::settings::flags;
use crate::settings::{flags, Settings};
use crate::test::test_path;
use crate::{assert_messages, directives, settings};

Expand All @@ -38,6 +39,7 @@ mod tests {
#[test_case(Rule::UnusedImport, Path::new("F401_12.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_13.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_14.py"))]
#[test_case(Rule::UnusedImport, Path::new("F401_15.py"))]
#[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"))]
#[test_case(Rule::UndefinedLocalWithImportStar, Path::new("F403.py"))]
#[test_case(Rule::LateFutureImport, Path::new("F404.py"))]
Expand Down Expand Up @@ -252,6 +254,22 @@ mod tests {
Ok(())
}

#[test]
fn extend_generics() -> Result<()> {
let snapshot = "extend_immutable_calls".to_string();
let diagnostics = test_path(
Path::new("pyflakes/F401_15.py"),
&Settings {
pyflakes: super::settings::Settings {
extend_generics: vec!["django.db.models.ForeignKey".to_string()],
},
..Settings::for_rules(vec![Rule::UnusedImport])
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}

/// A re-implementation of the Pyflakes test runner.
/// Note that all tests marked with `#[ignore]` should be considered TODOs.
fn flakes(contents: &str, expected: &[Rule]) {
Expand Down
4 changes: 4 additions & 0 deletions crates/ruff/src/rules/pyflakes/rules/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ pub(crate) enum UnusedImportContext {
/// If an import statement is used to check for the availability or existence
/// of a module, consider using `importlib.util.find_spec` instead.
///
/// ## Options
///
/// - `pyflakes.extend-generics`
///
/// ## Example
/// ```python
/// import numpy as np # unused import
Expand Down
47 changes: 47 additions & 0 deletions crates/ruff/src/rules/pyflakes/settings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//! Settings for the `Pyflakes` plugin.

use serde::{Deserialize, Serialize};

use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions};

#[derive(
Debug, PartialEq, Eq, Default, Serialize, Deserialize, ConfigurationOptions, CombineOptions,
)]
#[serde(
deny_unknown_fields,
rename_all = "kebab-case",
rename = "PyflakesOptions"
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Options {
#[option(
default = r#"[]"#,
value_type = "list[str]",
example = "extend-generics = [\"django.db.models.ForeignKey\"]"
)]
/// Additional functions or classes to consider generic, such that any
/// subscripts should be treated as type annotation (e.g., `ForeignKey` in
/// `django.db.models.ForeignKey["User"]`.
pub extend_generics: Option<Vec<String>>,
}

#[derive(Debug, Default, CacheKey)]
pub struct Settings {
pub extend_generics: Vec<String>,
}

impl From<Options> for Settings {
fn from(options: Options) -> Self {
Self {
extend_generics: options.extend_generics.unwrap_or_default(),
}
}
}

impl From<Settings> for Options {
fn from(settings: Settings) -> Self {
Self {
extend_generics: Some(settings.extend_generics),
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
source: crates/ruff/src/rules/pyflakes/mod.rs
---
F401_15.py:5:25: F401 [*] `pathlib.Path` imported but unused
|
5 | if TYPE_CHECKING:
6 | from pathlib import Path
| ^^^^ F401
|
= help: Remove unused import: `pathlib.Path`

ℹ Suggested fix
2 2 | from django.db.models import ForeignKey
3 3 |
4 4 | if TYPE_CHECKING:
5 |- from pathlib import Path
5 |+ pass
6 6 |
7 7 |
8 8 | class Foo:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm little late for the party, but isn't this PR supposed to eliminate this error?

I'm trying to test it, with following config:

[pyflakes]
extend-generics = ["django.db.models.ForeignKey"]
> cargo r --bin ruff -- crates/ruff/resources/test/fixtures/pyflakes/F401_15.py
...
crates/ruff/resources/test/fixtures/pyflakes/F401_15.py:5:25: F401 [*] `pathlib.Path` imported but unused

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This works if you put ruff.toml in the same folder as F401_15.py.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, great, thank you!



Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/pyflakes/mod.rs
---

5 changes: 4 additions & 1 deletion crates/ruff/src/settings/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::rules::{
flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions,
flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions,
flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking,
flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pylint,
flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint,
};
use crate::settings::options::Options;
use crate::settings::types::{
Expand Down Expand Up @@ -89,6 +89,7 @@ pub struct Configuration {
pub pep8_naming: Option<pep8_naming::settings::Options>,
pub pycodestyle: Option<pycodestyle::settings::Options>,
pub pydocstyle: Option<pydocstyle::settings::Options>,
pub pyflakes: Option<pyflakes::settings::Options>,
pub pylint: Option<pylint::settings::Options>,
}

Expand Down Expand Up @@ -241,6 +242,7 @@ impl Configuration {
pep8_naming: options.pep8_naming,
pycodestyle: options.pycodestyle,
pydocstyle: options.pydocstyle,
pyflakes: options.pyflakes,
pylint: options.pylint,
})
}
Expand Down Expand Up @@ -326,6 +328,7 @@ impl Configuration {
pep8_naming: self.pep8_naming.combine(config.pep8_naming),
pycodestyle: self.pycodestyle.combine(config.pycodestyle),
pydocstyle: self.pydocstyle.combine(config.pydocstyle),
pyflakes: self.pyflakes.combine(config.pyflakes),
pylint: self.pylint.combine(config.pylint),
}
}
Expand Down
3 changes: 2 additions & 1 deletion crates/ruff/src/settings/defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::rules::{
flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions,
flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions,
flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking,
flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pylint,
flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint,
};
use crate::settings::types::FilePatternSet;

Expand Down Expand Up @@ -110,6 +110,7 @@ impl Default for Settings {
pep8_naming: pep8_naming::settings::Settings::default(),
pycodestyle: pycodestyle::settings::Settings::default(),
pydocstyle: pydocstyle::settings::Settings::default(),
pyflakes: pyflakes::settings::Settings::default(),
pylint: pylint::settings::Settings::default(),
}
}
Expand Down
4 changes: 3 additions & 1 deletion crates/ruff/src/settings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::rules::{
flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions,
flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions,
flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking,
flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pylint,
flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint,
};
use crate::settings::configuration::Configuration;
use crate::settings::types::{FilePatternSet, PerFileIgnore, PythonVersion, SerializationFormat};
Expand Down Expand Up @@ -126,6 +126,7 @@ pub struct Settings {
pub pep8_naming: pep8_naming::settings::Settings,
pub pycodestyle: pycodestyle::settings::Settings,
pub pydocstyle: pydocstyle::settings::Settings,
pub pyflakes: pyflakes::settings::Settings,
pub pylint: pylint::settings::Settings,
}

Expand Down Expand Up @@ -231,6 +232,7 @@ impl Settings {
pep8_naming: config.pep8_naming.map(Into::into).unwrap_or_default(),
pycodestyle: config.pycodestyle.map(Into::into).unwrap_or_default(),
pydocstyle: config.pydocstyle.map(Into::into).unwrap_or_default(),
pyflakes: config.pyflakes.map(Into::into).unwrap_or_default(),
pylint: config.pylint.map(Into::into).unwrap_or_default(),
})
}
Expand Down
5 changes: 4 additions & 1 deletion crates/ruff/src/settings/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::rules::{
flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions,
flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions,
flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking,
flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pylint,
flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint,
};
use crate::settings::types::{PythonVersion, SerializationFormat, Version};

Expand Down Expand Up @@ -539,6 +539,9 @@ pub struct Options {
/// Options for the `pydocstyle` plugin.
pub pydocstyle: Option<pydocstyle::settings::Options>,
#[option_group]
/// Options for the `pyflakes` plugin.
pub pyflakes: Option<pyflakes::settings::Options>,
#[option_group]
/// Options for the `pylint` plugin.
pub pylint: Option<pylint::settings::Options>,
// Tables are required to go last.
Expand Down
10 changes: 8 additions & 2 deletions crates/ruff_python_semantic/src/analyze/typing.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use rustpython_parser::ast::{self, Constant, Expr, Operator};

use num_traits::identities::Zero;
use ruff_python_ast::call_path::{from_unqualified_name, CallPath};
use ruff_python_ast::call_path::{from_qualified_name, from_unqualified_name, CallPath};
use ruff_python_stdlib::typing::{
IMMUTABLE_GENERIC_TYPES, IMMUTABLE_TYPES, PEP_585_GENERICS, PEP_593_SUBSCRIPTS, SUBSCRIPTS,
};
Expand Down Expand Up @@ -29,6 +29,7 @@ pub fn match_annotated_subscript<'a>(
expr: &Expr,
semantic_model: &SemanticModel,
typing_modules: impl Iterator<Item = &'a str>,
extend_generics: &[String],
) -> Option<SubscriptKind> {
if !matches!(expr, Expr::Name(_) | Expr::Attribute(_)) {
return None;
Expand All @@ -37,7 +38,12 @@ pub fn match_annotated_subscript<'a>(
semantic_model
.resolve_call_path(expr)
.and_then(|call_path| {
if SUBSCRIPTS.contains(&call_path.as_slice()) {
if SUBSCRIPTS.contains(&call_path.as_slice())
|| extend_generics
.iter()
.map(|target| from_qualified_name(target))
.any(|target| call_path == target)
{
return Some(SubscriptKind::AnnotatedSubscript);
}
if PEP_593_SUBSCRIPTS.contains(&call_path.as_slice()) {
Expand Down
3 changes: 2 additions & 1 deletion crates/ruff_wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use ruff::rules::{
flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions,
flake8_errmsg, flake8_gettext, flake8_implicit_str_concat, flake8_import_conventions,
flake8_pytest_style, flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking,
flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pylint,
flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pylint,
};
use ruff::settings::configuration::Configuration;
use ruff::settings::options::Options;
Expand Down Expand Up @@ -158,6 +158,7 @@ pub fn defaultSettings() -> Result<JsValue, JsValue> {
pep8_naming: Some(pep8_naming::settings::Settings::default().into()),
pycodestyle: Some(pycodestyle::settings::Settings::default().into()),
pydocstyle: Some(pydocstyle::settings::Settings::default().into()),
pyflakes: Some(pyflakes::settings::Settings::default().into()),
pylint: Some(pylint::settings::Settings::default().into()),
})?)
}
Expand Down
27 changes: 27 additions & 0 deletions ruff.schema.json

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