From b9c53a74f97a153637c213d7e3c69c870e7c8aeb Mon Sep 17 00:00:00 2001 From: InSync Date: Wed, 20 Nov 2024 10:08:15 +0700 Subject: [PATCH] [`pycodestyle`] Exempt `pytest.importorskip()` calls (`E402`) (#14474) ## Summary Resolves #13537. ## Test Plan `cargo nextest run` and `cargo insta test`. --- .../test/fixtures/pycodestyle/E402_3.py | 8 +++++++ crates/ruff_linter/src/checkers/ast/mod.rs | 5 ++++- .../ruff_linter/src/rules/pycodestyle/mod.rs | 5 +++-- ...style__tests__preview__E402_E402_3.py.snap | 5 +++++ .../src/analyze/imports.rs | 21 +++++++++++++++++++ 5 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_3.py create mode 100644 crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_3.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_3.py b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_3.py new file mode 100644 index 0000000000..d7faff834a --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_3.py @@ -0,0 +1,8 @@ +import pytest + +pytest.importorskip("foo.bar") + +import re +from sys import version + +from numpy import * diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 87f9b27698..2421565b6b 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -453,6 +453,8 @@ impl<'a> Checker<'a> { impl<'a> Visitor<'a> for Checker<'a> { fn visit_stmt(&mut self, stmt: &'a Stmt) { + let in_preview = self.settings.preview.is_enabled(); + // Step 0: Pre-processing self.semantic.push_node(stmt); @@ -504,7 +506,8 @@ impl<'a> Visitor<'a> for Checker<'a> { || helpers::in_nested_block(self.semantic.current_statements()) || imports::is_matplotlib_activation(stmt, self.semantic()) || imports::is_sys_path_modification(stmt, self.semantic()) - || imports::is_os_environ_modification(stmt, self.semantic())) + || imports::is_os_environ_modification(stmt, self.semantic()) + || (in_preview && imports::is_pytest_importorskip(stmt, self.semantic()))) { self.semantic.flags |= SemanticModelFlags::IMPORT_BOUNDARY; } diff --git a/crates/ruff_linter/src/rules/pycodestyle/mod.rs b/crates/ruff_linter/src/rules/pycodestyle/mod.rs index 9850b51d89..f78170c5d4 100644 --- a/crates/ruff_linter/src/rules/pycodestyle/mod.rs +++ b/crates/ruff_linter/src/rules/pycodestyle/mod.rs @@ -72,14 +72,15 @@ mod tests { Ok(()) } + #[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_3.py"))] #[test_case(Rule::RedundantBackslash, Path::new("E502.py"))] + // E741 has different behaviour for `.pyi` files in preview mode + #[test_case(Rule::AmbiguousVariableName, Path::new("E741.pyi"))] #[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_0.py"))] #[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_1.py"))] #[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_2.py"))] #[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_3.py"))] #[test_case(Rule::TooManyNewlinesAtEndOfFile, Path::new("W391_4.py"))] - // E741 has different behaviour for `.pyi` files in preview mode - #[test_case(Rule::AmbiguousVariableName, Path::new("E741.pyi"))] fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!( "preview__{}_{}", diff --git a/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_3.py.snap b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_3.py.snap new file mode 100644 index 0000000000..e5d90e3bfa --- /dev/null +++ b/crates/ruff_linter/src/rules/pycodestyle/snapshots/ruff_linter__rules__pycodestyle__tests__preview__E402_E402_3.py.snap @@ -0,0 +1,5 @@ +--- +source: crates/ruff_linter/src/rules/pycodestyle/mod.rs +snapshot_kind: text +--- + diff --git a/crates/ruff_python_semantic/src/analyze/imports.rs b/crates/ruff_python_semantic/src/analyze/imports.rs index 162cb6e653..a88103693b 100644 --- a/crates/ruff_python_semantic/src/analyze/imports.rs +++ b/crates/ruff_python_semantic/src/analyze/imports.rs @@ -102,3 +102,24 @@ pub fn is_matplotlib_activation(stmt: &Stmt, semantic: &SemanticModel) -> bool { .resolve_qualified_name(func.as_ref()) .is_some_and(|qualified_name| matches!(qualified_name.segments(), ["matplotlib", "use"])) } + +/// Returns `true` if a [`Stmt`] is a `pytest.importorskip()` call, as in: +/// ```python +/// import pytest +/// +/// pytest.importorskip("foo.bar") +/// ``` +pub fn is_pytest_importorskip(stmt: &Stmt, semantic: &SemanticModel) -> bool { + let Stmt::Expr(ast::StmtExpr { value, .. }) = stmt else { + return false; + }; + let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else { + return false; + }; + + semantic + .resolve_qualified_name(func.as_ref()) + .is_some_and(|qualified_name| { + matches!(qualified_name.segments(), ["pytest", "importorskip"]) + }) +}