From 4c8d6121203ee5ee39d581b5cf5a118c464d15e6 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 19 Jun 2025 05:49:34 -0400 Subject: [PATCH] Enforce `pytest` import for decorators (#18779) --- .../fixtures/flake8_pytest_style/PT023.py | 5 + .../flake8_pytest_style/rules/fixture.rs | 2 +- .../flake8_pytest_style/rules/helpers.rs | 27 ++-- .../rules/flake8_pytest_style/rules/marks.rs | 2 +- ...e8_pytest_style__tests__PT023_default.snap | 125 +++++++++-------- ...ytest_style__tests__PT023_parentheses.snap | 127 +++++++++--------- 6 files changed, 147 insertions(+), 141 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT023.py b/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT023.py index 91a0605942..b212ded929 100644 --- a/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT023.py +++ b/crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT023.py @@ -1,3 +1,8 @@ +@pytest.mark.foo(scope="module") +def ok_due_to_missing_import(): + pass + + import pytest diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs index 433aca9834..9f7138336c 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/fixture.rs @@ -896,7 +896,7 @@ fn check_fixture_addfinalizer(checker: &Checker, parameters: &Parameters, body: /// PT024, PT025 fn check_fixture_marks(checker: &Checker, decorators: &[Decorator]) { - for (expr, marker) in get_mark_decorators(decorators) { + for (expr, marker) in get_mark_decorators(decorators, checker.semantic()) { if checker.enabled(Rule::PytestUnnecessaryAsyncioMarkOnFixture) { if marker == "asyncio" { let mut diagnostic = diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/helpers.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/helpers.rs index 24d3a987a6..43429c8c4f 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/helpers.rs @@ -1,21 +1,24 @@ -use crate::checkers::ast::Checker; +use std::fmt; + use ruff_python_ast::helpers::map_callable; -use ruff_python_ast::name::UnqualifiedName; use ruff_python_ast::{self as ast, Decorator, Expr, ExprCall, Keyword, Stmt, StmtFunctionDef}; use ruff_python_semantic::analyze::visibility; use ruff_python_semantic::{ScopeKind, SemanticModel}; use ruff_python_trivia::PythonWhitespace; -use std::fmt; -pub(super) fn get_mark_decorators( - decorators: &[Decorator], -) -> impl Iterator { - decorators.iter().filter_map(|decorator| { - let name = UnqualifiedName::from_expr(map_callable(&decorator.expression))?; - let ["pytest", "mark", marker] = name.segments() else { - return None; - }; - Some((decorator, *marker)) +use crate::checkers::ast::Checker; + +pub(super) fn get_mark_decorators<'a>( + decorators: &'a [Decorator], + semantic: &'a SemanticModel, +) -> impl Iterator + 'a { + decorators.iter().filter_map(move |decorator| { + let expr = map_callable(&decorator.expression); + let qualified_name = semantic.resolve_qualified_name(expr)?; + match qualified_name.segments() { + ["pytest", "mark", marker] => Some((decorator, *marker)), + _ => None, + } }) } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/marks.rs b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/marks.rs index 8b80c6b289..36bbd312a6 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/rules/marks.rs +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/rules/marks.rs @@ -214,7 +214,7 @@ pub(crate) fn marks(checker: &Checker, decorators: &[Decorator]) { let enforce_parentheses = checker.enabled(Rule::PytestIncorrectMarkParenthesesStyle); let enforce_useless_usefixtures = checker.enabled(Rule::PytestUseFixturesWithoutParameters); - for (decorator, marker) in get_mark_decorators(decorators) { + for (decorator, marker) in get_mark_decorators(decorators, checker.semantic()) { if enforce_parentheses { check_mark_parentheses(checker, decorator, marker); } diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT023_default.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT023_default.snap index 8240fc440b..20466f29fe 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT023_default.snap +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT023_default.snap @@ -1,101 +1,100 @@ --- source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs -snapshot_kind: text --- -PT023.py:46:1: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` - | -46 | @pytest.mark.foo() - | ^^^^^^^^^^^^^^^^^^ PT023 -47 | def test_something(): -48 | pass - | - = help: Remove parentheses - -ℹ Safe fix -43 43 | # With parentheses -44 44 | -45 45 | -46 |-@pytest.mark.foo() - 46 |+@pytest.mark.foo -47 47 | def test_something(): -48 48 | pass -49 49 | - PT023.py:51:1: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` | 51 | @pytest.mark.foo() | ^^^^^^^^^^^^^^^^^^ PT023 -52 | class TestClass: -53 | def test_something(): +52 | def test_something(): +53 | pass | = help: Remove parentheses ℹ Safe fix -48 48 | pass +48 48 | # With parentheses 49 49 | 50 50 | 51 |-@pytest.mark.foo() 51 |+@pytest.mark.foo -52 52 | class TestClass: -53 53 | def test_something(): -54 54 | pass +52 52 | def test_something(): +53 53 | pass +54 54 | -PT023.py:58:5: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` +PT023.py:56:1: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` | +56 | @pytest.mark.foo() + | ^^^^^^^^^^^^^^^^^^ PT023 57 | class TestClass: -58 | @pytest.mark.foo() - | ^^^^^^^^^^^^^^^^^^ PT023 -59 | def test_something(): -60 | pass +58 | def test_something(): | = help: Remove parentheses ℹ Safe fix +53 53 | pass +54 54 | 55 55 | -56 56 | +56 |-@pytest.mark.foo() + 56 |+@pytest.mark.foo 57 57 | class TestClass: -58 |- @pytest.mark.foo() - 58 |+ @pytest.mark.foo -59 59 | def test_something(): -60 60 | pass -61 61 | +58 58 | def test_something(): +59 59 | pass -PT023.py:64:5: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` +PT023.py:63:5: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` | -63 | class TestClass: -64 | @pytest.mark.foo() +62 | class TestClass: +63 | @pytest.mark.foo() | ^^^^^^^^^^^^^^^^^^ PT023 -65 | class TestNestedClass: -66 | def test_something(): +64 | def test_something(): +65 | pass | = help: Remove parentheses ℹ Safe fix +60 60 | 61 61 | -62 62 | -63 63 | class TestClass: -64 |- @pytest.mark.foo() - 64 |+ @pytest.mark.foo -65 65 | class TestNestedClass: -66 66 | def test_something(): -67 67 | pass +62 62 | class TestClass: +63 |- @pytest.mark.foo() + 63 |+ @pytest.mark.foo +64 64 | def test_something(): +65 65 | pass +66 66 | -PT023.py:72:9: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` +PT023.py:69:5: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` | -70 | class TestClass: -71 | class TestNestedClass: -72 | @pytest.mark.foo() - | ^^^^^^^^^^^^^^^^^^ PT023 -73 | def test_something(): -74 | pass +68 | class TestClass: +69 | @pytest.mark.foo() + | ^^^^^^^^^^^^^^^^^^ PT023 +70 | class TestNestedClass: +71 | def test_something(): | = help: Remove parentheses ℹ Safe fix -69 69 | -70 70 | class TestClass: -71 71 | class TestNestedClass: -72 |- @pytest.mark.foo() - 72 |+ @pytest.mark.foo -73 73 | def test_something(): -74 74 | pass +66 66 | +67 67 | +68 68 | class TestClass: +69 |- @pytest.mark.foo() + 69 |+ @pytest.mark.foo +70 70 | class TestNestedClass: +71 71 | def test_something(): +72 72 | pass + +PT023.py:77:9: PT023 [*] Use `@pytest.mark.foo` over `@pytest.mark.foo()` + | +75 | class TestClass: +76 | class TestNestedClass: +77 | @pytest.mark.foo() + | ^^^^^^^^^^^^^^^^^^ PT023 +78 | def test_something(): +79 | pass + | + = help: Remove parentheses + +ℹ Safe fix +74 74 | +75 75 | class TestClass: +76 76 | class TestNestedClass: +77 |- @pytest.mark.foo() + 77 |+ @pytest.mark.foo +78 78 | def test_something(): +79 79 | pass diff --git a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT023_parentheses.snap b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT023_parentheses.snap index 4021157c87..a992327458 100644 --- a/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT023_parentheses.snap +++ b/crates/ruff_linter/src/rules/flake8_pytest_style/snapshots/ruff_linter__rules__flake8_pytest_style__tests__PT023_parentheses.snap @@ -1,102 +1,101 @@ --- source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs -snapshot_kind: text --- -PT023.py:12:1: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` - | -12 | @pytest.mark.foo - | ^^^^^^^^^^^^^^^^ PT023 -13 | def test_something(): -14 | pass - | - = help: Add parentheses - -ℹ Safe fix -9 9 | # Without parentheses -10 10 | -11 11 | -12 |-@pytest.mark.foo - 12 |+@pytest.mark.foo() -13 13 | def test_something(): -14 14 | pass -15 15 | - PT023.py:17:1: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` | 17 | @pytest.mark.foo | ^^^^^^^^^^^^^^^^ PT023 -18 | class TestClass: -19 | def test_something(): +18 | def test_something(): +19 | pass | = help: Add parentheses ℹ Safe fix -14 14 | pass +14 14 | # Without parentheses 15 15 | 16 16 | 17 |-@pytest.mark.foo 17 |+@pytest.mark.foo() -18 18 | class TestClass: -19 19 | def test_something(): -20 20 | pass +18 18 | def test_something(): +19 19 | pass +20 20 | -PT023.py:24:5: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` +PT023.py:22:1: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` | +22 | @pytest.mark.foo + | ^^^^^^^^^^^^^^^^ PT023 23 | class TestClass: -24 | @pytest.mark.foo - | ^^^^^^^^^^^^^^^^ PT023 -25 | def test_something(): -26 | pass +24 | def test_something(): | = help: Add parentheses ℹ Safe fix +19 19 | pass +20 20 | 21 21 | -22 22 | +22 |-@pytest.mark.foo + 22 |+@pytest.mark.foo() 23 23 | class TestClass: -24 |- @pytest.mark.foo - 24 |+ @pytest.mark.foo() -25 25 | def test_something(): -26 26 | pass -27 27 | +24 24 | def test_something(): +25 25 | pass -PT023.py:30:5: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` +PT023.py:29:5: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` | -29 | class TestClass: -30 | @pytest.mark.foo +28 | class TestClass: +29 | @pytest.mark.foo | ^^^^^^^^^^^^^^^^ PT023 -31 | class TestNestedClass: -32 | def test_something(): +30 | def test_something(): +31 | pass | = help: Add parentheses ℹ Safe fix +26 26 | 27 27 | -28 28 | -29 29 | class TestClass: -30 |- @pytest.mark.foo - 30 |+ @pytest.mark.foo() -31 31 | class TestNestedClass: -32 32 | def test_something(): -33 33 | pass +28 28 | class TestClass: +29 |- @pytest.mark.foo + 29 |+ @pytest.mark.foo() +30 30 | def test_something(): +31 31 | pass +32 32 | -PT023.py:38:9: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` +PT023.py:35:5: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` | -36 | class TestClass: -37 | class TestNestedClass: -38 | @pytest.mark.foo - | ^^^^^^^^^^^^^^^^ PT023 -39 | def test_something(): -40 | pass +34 | class TestClass: +35 | @pytest.mark.foo + | ^^^^^^^^^^^^^^^^ PT023 +36 | class TestNestedClass: +37 | def test_something(): | = help: Add parentheses ℹ Safe fix -35 35 | -36 36 | class TestClass: -37 37 | class TestNestedClass: -38 |- @pytest.mark.foo - 38 |+ @pytest.mark.foo() -39 39 | def test_something(): -40 40 | pass -41 41 | +32 32 | +33 33 | +34 34 | class TestClass: +35 |- @pytest.mark.foo + 35 |+ @pytest.mark.foo() +36 36 | class TestNestedClass: +37 37 | def test_something(): +38 38 | pass + +PT023.py:43:9: PT023 [*] Use `@pytest.mark.foo()` over `@pytest.mark.foo` + | +41 | class TestClass: +42 | class TestNestedClass: +43 | @pytest.mark.foo + | ^^^^^^^^^^^^^^^^ PT023 +44 | def test_something(): +45 | pass + | + = help: Add parentheses + +ℹ Safe fix +40 40 | +41 41 | class TestClass: +42 42 | class TestNestedClass: +43 |- @pytest.mark.foo + 43 |+ @pytest.mark.foo() +44 44 | def test_something(): +45 45 | pass +46 46 |