mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-03 07:04:53 +00:00
[flake8-tidy-imports
] Add TID253
(#6378)
## Summary Add a new rule `TID253` (`banned-module-level-imports`), to ban a user-specified list of imports from appearing at module level. This rule doesn't exist in `flake8-tidy-imports`, so it's unique to Ruff. The implementation is pretty similar to `TID251`. Briefly discussed [here](https://github.com/astral-sh/ruff/discussions/6370). ## Test Plan Added a new test case, checking that inline imports are allowed and that non-inline imports from the banned list are disallowed.
This commit is contained in:
parent
a1da9da0ef
commit
c03e2acadb
10 changed files with 303 additions and 0 deletions
31
crates/ruff/resources/test/fixtures/flake8_tidy_imports/TID253.py
vendored
Normal file
31
crates/ruff/resources/test/fixtures/flake8_tidy_imports/TID253.py
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
## Banned modules ##
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from torch import *
|
||||||
|
|
||||||
|
from tensorflow import a, b, c
|
||||||
|
|
||||||
|
import torch as torch_wearing_a_trenchcoat
|
||||||
|
|
||||||
|
# this should count as module level
|
||||||
|
x = 1; import tensorflow
|
||||||
|
|
||||||
|
# banning a module also bans any submodules
|
||||||
|
import torch.foo.bar
|
||||||
|
|
||||||
|
from tensorflow.foo import bar
|
||||||
|
|
||||||
|
from torch.foo.bar import *
|
||||||
|
|
||||||
|
# unlike TID251, inline imports are *not* banned
|
||||||
|
def my_cool_function():
|
||||||
|
import tensorflow.foo.bar
|
||||||
|
|
||||||
|
def another_cool_function():
|
||||||
|
from torch.foo import bar
|
||||||
|
|
||||||
|
def import_alias():
|
||||||
|
from torch.foo import bar
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import torch
|
|
@ -566,6 +566,15 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||||
alias,
|
alias,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if checker.enabled(Rule::BannedModuleLevelImports) {
|
||||||
|
flake8_tidy_imports::rules::name_or_parent_is_banned_at_module_level(
|
||||||
|
checker,
|
||||||
|
&alias.name,
|
||||||
|
alias.range(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if !checker.source_type.is_stub() {
|
if !checker.source_type.is_stub() {
|
||||||
if checker.enabled(Rule::UselessImportAlias) {
|
if checker.enabled(Rule::UselessImportAlias) {
|
||||||
pylint::rules::useless_import_alias(checker, alias);
|
pylint::rules::useless_import_alias(checker, alias);
|
||||||
|
@ -734,6 +743,28 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if checker.enabled(Rule::BannedModuleLevelImports) {
|
||||||
|
if let Some(module) =
|
||||||
|
helpers::resolve_imported_module_path(level, module, checker.module_path)
|
||||||
|
{
|
||||||
|
flake8_tidy_imports::rules::name_or_parent_is_banned_at_module_level(
|
||||||
|
checker,
|
||||||
|
&module,
|
||||||
|
stmt.range(),
|
||||||
|
);
|
||||||
|
|
||||||
|
for alias in names {
|
||||||
|
if &alias.name == "*" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
flake8_tidy_imports::rules::name_is_banned_at_module_level(
|
||||||
|
checker,
|
||||||
|
&format!("{module}.{}", alias.name),
|
||||||
|
alias.range(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if checker.enabled(Rule::PytestIncorrectPytestImport) {
|
if checker.enabled(Rule::PytestIncorrectPytestImport) {
|
||||||
if let Some(diagnostic) =
|
if let Some(diagnostic) =
|
||||||
flake8_pytest_style::rules::import_from(stmt, module, level)
|
flake8_pytest_style::rules::import_from(stmt, module, level)
|
||||||
|
|
|
@ -311,6 +311,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||||
// flake8-tidy-imports
|
// flake8-tidy-imports
|
||||||
(Flake8TidyImports, "251") => (RuleGroup::Unspecified, rules::flake8_tidy_imports::rules::BannedApi),
|
(Flake8TidyImports, "251") => (RuleGroup::Unspecified, rules::flake8_tidy_imports::rules::BannedApi),
|
||||||
(Flake8TidyImports, "252") => (RuleGroup::Unspecified, rules::flake8_tidy_imports::rules::RelativeImports),
|
(Flake8TidyImports, "252") => (RuleGroup::Unspecified, rules::flake8_tidy_imports::rules::RelativeImports),
|
||||||
|
(Flake8TidyImports, "253") => (RuleGroup::Unspecified, rules::flake8_tidy_imports::rules::BannedModuleLevelImports),
|
||||||
|
|
||||||
// flake8-return
|
// flake8-return
|
||||||
(Flake8Return, "501") => (RuleGroup::Unspecified, rules::flake8_return::rules::UnnecessaryReturnNone),
|
(Flake8Return, "501") => (RuleGroup::Unspecified, rules::flake8_return::rules::UnnecessaryReturnNone),
|
||||||
|
|
|
@ -124,4 +124,23 @@ mod tests {
|
||||||
assert_messages!(diagnostics);
|
assert_messages!(diagnostics);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn banned_module_level_imports() -> Result<()> {
|
||||||
|
let diagnostics = test_path(
|
||||||
|
Path::new("flake8_tidy_imports/TID253.py"),
|
||||||
|
&Settings {
|
||||||
|
flake8_tidy_imports: flake8_tidy_imports::settings::Settings {
|
||||||
|
banned_module_level_imports: vec![
|
||||||
|
"torch".to_string(),
|
||||||
|
"tensorflow".to_string(),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Settings::for_rules(vec![Rule::BannedModuleLevelImports])
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
assert_messages!(diagnostics);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,19 @@ pub struct Options {
|
||||||
/// Note that this rule is only meant to flag accidental uses,
|
/// Note that this rule is only meant to flag accidental uses,
|
||||||
/// and can be circumvented via `eval` or `importlib`.
|
/// and can be circumvented via `eval` or `importlib`.
|
||||||
pub banned_api: Option<FxHashMap<String, ApiBan>>,
|
pub banned_api: Option<FxHashMap<String, ApiBan>>,
|
||||||
|
#[option(
|
||||||
|
default = r#"[]"#,
|
||||||
|
value_type = r#"list[str]"#,
|
||||||
|
example = r#"
|
||||||
|
# Ban certain modules from being imported at module level, instead requiring
|
||||||
|
# that they're imported lazily (e.g., within a function definition).
|
||||||
|
banned-module-level-imports = ["torch", "tensorflow"]
|
||||||
|
"#
|
||||||
|
)]
|
||||||
|
/// List of specific modules that may not be imported at module level, and should instead be
|
||||||
|
/// imported lazily (e.g., within a function definition, or an `if TYPE_CHECKING:`
|
||||||
|
/// block, or some other nested context).
|
||||||
|
pub banned_module_level_imports: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Options> for Settings {
|
impl From<Options> for Settings {
|
||||||
|
@ -48,6 +61,7 @@ impl From<Options> for Settings {
|
||||||
Self {
|
Self {
|
||||||
ban_relative_imports: options.ban_relative_imports.unwrap_or(Strictness::Parents),
|
ban_relative_imports: options.ban_relative_imports.unwrap_or(Strictness::Parents),
|
||||||
banned_api: options.banned_api.unwrap_or_default(),
|
banned_api: options.banned_api.unwrap_or_default(),
|
||||||
|
banned_module_level_imports: options.banned_module_level_imports.unwrap_or_default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,6 +71,7 @@ impl From<Settings> for Options {
|
||||||
Self {
|
Self {
|
||||||
ban_relative_imports: Some(settings.ban_relative_imports),
|
ban_relative_imports: Some(settings.ban_relative_imports),
|
||||||
banned_api: Some(settings.banned_api),
|
banned_api: Some(settings.banned_api),
|
||||||
|
banned_module_level_imports: Some(settings.banned_module_level_imports),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
use ruff_diagnostics::{Diagnostic, Violation};
|
||||||
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
|
use ruff_text_size::TextRange;
|
||||||
|
|
||||||
|
use crate::checkers::ast::Checker;
|
||||||
|
|
||||||
|
/// ## What it does
|
||||||
|
/// Checks for module-level imports that should instead be imported lazily
|
||||||
|
/// (e.g., within a function definition, or an `if TYPE_CHECKING:` block, or
|
||||||
|
/// some other nested context).
|
||||||
|
///
|
||||||
|
/// ## Why is this bad?
|
||||||
|
/// Some modules are expensive to import. For example, importing `torch` or
|
||||||
|
/// `tensorflow` can introduce a noticeable delay in the startup time of a
|
||||||
|
/// Python program.
|
||||||
|
///
|
||||||
|
/// In such cases, you may want to enforce that the module is imported lazily
|
||||||
|
/// as needed, rather than at the top of the file. This could involve inlining
|
||||||
|
/// the import into the function that uses it, rather than importing it
|
||||||
|
/// unconditionally, to ensure that the module is only imported when necessary.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
/// ```python
|
||||||
|
/// import tensorflow as tf
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// def show_version():
|
||||||
|
/// print(tf.__version__)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Use instead:
|
||||||
|
/// ```python
|
||||||
|
/// def show_version():
|
||||||
|
/// import tensorflow as tf
|
||||||
|
///
|
||||||
|
/// print(tf.__version__)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## Options
|
||||||
|
/// - `flake8-tidy-imports.banned-module-level-imports`
|
||||||
|
#[violation]
|
||||||
|
pub struct BannedModuleLevelImports {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Violation for BannedModuleLevelImports {
|
||||||
|
#[derive_message_formats]
|
||||||
|
fn message(&self) -> String {
|
||||||
|
let BannedModuleLevelImports { name } = self;
|
||||||
|
format!("`{name}` is banned at the module level")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TID253
|
||||||
|
pub(crate) fn name_is_banned_at_module_level(
|
||||||
|
checker: &mut Checker,
|
||||||
|
name: &str,
|
||||||
|
text_range: TextRange,
|
||||||
|
) {
|
||||||
|
banned_at_module_level_with_policy(checker, name, text_range, &NameMatchPolicy::ExactOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TID253
|
||||||
|
pub(crate) fn name_or_parent_is_banned_at_module_level(
|
||||||
|
checker: &mut Checker,
|
||||||
|
name: &str,
|
||||||
|
text_range: TextRange,
|
||||||
|
) {
|
||||||
|
banned_at_module_level_with_policy(checker, name, text_range, &NameMatchPolicy::ExactOrParents);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum NameMatchPolicy {
|
||||||
|
/// Only match an exact module name (e.g., given `import foo.bar`, only match `foo.bar`).
|
||||||
|
ExactOnly,
|
||||||
|
/// Match an exact module name or any of its parents (e.g., given `import foo.bar`, match
|
||||||
|
/// `foo.bar` or `foo`).
|
||||||
|
ExactOrParents,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn banned_at_module_level_with_policy(
|
||||||
|
checker: &mut Checker,
|
||||||
|
name: &str,
|
||||||
|
text_range: TextRange,
|
||||||
|
policy: &NameMatchPolicy,
|
||||||
|
) {
|
||||||
|
if !checker.semantic().at_top_level() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let banned_module_level_imports = &checker
|
||||||
|
.settings
|
||||||
|
.flake8_tidy_imports
|
||||||
|
.banned_module_level_imports;
|
||||||
|
for banned_module_name in banned_module_level_imports {
|
||||||
|
let name_is_banned = match policy {
|
||||||
|
NameMatchPolicy::ExactOnly => name == banned_module_name,
|
||||||
|
NameMatchPolicy::ExactOrParents => {
|
||||||
|
name == banned_module_name || name.starts_with(&format!("{banned_module_name}."))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if name_is_banned {
|
||||||
|
checker.diagnostics.push(Diagnostic::new(
|
||||||
|
BannedModuleLevelImports {
|
||||||
|
name: banned_module_name.to_string(),
|
||||||
|
},
|
||||||
|
text_range,
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
pub(crate) use banned_api::*;
|
pub(crate) use banned_api::*;
|
||||||
|
pub(crate) use banned_module_level_imports::*;
|
||||||
pub(crate) use relative_imports::*;
|
pub(crate) use relative_imports::*;
|
||||||
|
|
||||||
mod banned_api;
|
mod banned_api;
|
||||||
|
mod banned_module_level_imports;
|
||||||
mod relative_imports;
|
mod relative_imports;
|
||||||
|
|
|
@ -26,4 +26,5 @@ pub enum Strictness {
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
pub ban_relative_imports: Strictness,
|
pub ban_relative_imports: Strictness,
|
||||||
pub banned_api: FxHashMap<String, ApiBan>,
|
pub banned_api: FxHashMap<String, ApiBan>,
|
||||||
|
pub banned_module_level_imports: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
---
|
||||||
|
source: crates/ruff/src/rules/flake8_tidy_imports/mod.rs
|
||||||
|
---
|
||||||
|
TID253.py:2:8: TID253 `torch` is banned at the module level
|
||||||
|
|
|
||||||
|
1 | ## Banned modules ##
|
||||||
|
2 | import torch
|
||||||
|
| ^^^^^ TID253
|
||||||
|
3 |
|
||||||
|
4 | from torch import *
|
||||||
|
|
|
||||||
|
|
||||||
|
TID253.py:4:1: TID253 `torch` is banned at the module level
|
||||||
|
|
|
||||||
|
2 | import torch
|
||||||
|
3 |
|
||||||
|
4 | from torch import *
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^ TID253
|
||||||
|
5 |
|
||||||
|
6 | from tensorflow import a, b, c
|
||||||
|
|
|
||||||
|
|
||||||
|
TID253.py:6:1: TID253 `tensorflow` is banned at the module level
|
||||||
|
|
|
||||||
|
4 | from torch import *
|
||||||
|
5 |
|
||||||
|
6 | from tensorflow import a, b, c
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TID253
|
||||||
|
7 |
|
||||||
|
8 | import torch as torch_wearing_a_trenchcoat
|
||||||
|
|
|
||||||
|
|
||||||
|
TID253.py:8:8: TID253 `torch` is banned at the module level
|
||||||
|
|
|
||||||
|
6 | from tensorflow import a, b, c
|
||||||
|
7 |
|
||||||
|
8 | import torch as torch_wearing_a_trenchcoat
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TID253
|
||||||
|
9 |
|
||||||
|
10 | # this should count as module level
|
||||||
|
|
|
||||||
|
|
||||||
|
TID253.py:11:15: TID253 `tensorflow` is banned at the module level
|
||||||
|
|
|
||||||
|
10 | # this should count as module level
|
||||||
|
11 | x = 1; import tensorflow
|
||||||
|
| ^^^^^^^^^^ TID253
|
||||||
|
12 |
|
||||||
|
13 | # banning a module also bans any submodules
|
||||||
|
|
|
||||||
|
|
||||||
|
TID253.py:14:8: TID253 `torch` is banned at the module level
|
||||||
|
|
|
||||||
|
13 | # banning a module also bans any submodules
|
||||||
|
14 | import torch.foo.bar
|
||||||
|
| ^^^^^^^^^^^^^ TID253
|
||||||
|
15 |
|
||||||
|
16 | from tensorflow.foo import bar
|
||||||
|
|
|
||||||
|
|
||||||
|
TID253.py:16:1: TID253 `tensorflow` is banned at the module level
|
||||||
|
|
|
||||||
|
14 | import torch.foo.bar
|
||||||
|
15 |
|
||||||
|
16 | from tensorflow.foo import bar
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TID253
|
||||||
|
17 |
|
||||||
|
18 | from torch.foo.bar import *
|
||||||
|
|
|
||||||
|
|
||||||
|
TID253.py:18:1: TID253 `torch` is banned at the module level
|
||||||
|
|
|
||||||
|
16 | from tensorflow.foo import bar
|
||||||
|
17 |
|
||||||
|
18 | from torch.foo.bar import *
|
||||||
|
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ TID253
|
||||||
|
19 |
|
||||||
|
20 | # unlike TID251, inline imports are *not* banned
|
||||||
|
|
|
||||||
|
|
||||||
|
|
11
ruff.schema.json
generated
11
ruff.schema.json
generated
|
@ -1062,6 +1062,16 @@
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"$ref": "#/definitions/ApiBan"
|
"$ref": "#/definitions/ApiBan"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"banned-module-level-imports": {
|
||||||
|
"description": "List of specific modules that may not be imported at module level, and should instead be imported lazily (e.g., within a function definition, or an `if TYPE_CHECKING:` block, or some other nested context).",
|
||||||
|
"type": [
|
||||||
|
"array",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
@ -2610,6 +2620,7 @@
|
||||||
"TID25",
|
"TID25",
|
||||||
"TID251",
|
"TID251",
|
||||||
"TID252",
|
"TID252",
|
||||||
|
"TID253",
|
||||||
"TRY",
|
"TRY",
|
||||||
"TRY0",
|
"TRY0",
|
||||||
"TRY00",
|
"TRY00",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue