From 799c9790d10ee03ad6e3b2cf21c5633efa0d9bdf Mon Sep 17 00:00:00 2001 From: harupy Date: Mon, 14 Aug 2023 20:24:05 +0900 Subject: [PATCH 1/6] PT027 --- .../fixtures/flake8_pytest_style/PT027_0.py | 45 ++++ .../fixtures/flake8_pytest_style/PT027_1.py | 12 ++ .../src/checkers/ast/analyze/expression.rs | 7 + crates/ruff/src/codes.rs | 1 + .../ruff/src/rules/flake8_pytest_style/mod.rs | 12 ++ .../flake8_pytest_style/rules/assertion.rs | 176 ++++++++++++++++ ...__flake8_pytest_style__tests__PT027_0.snap | 196 ++++++++++++++++++ ...__flake8_pytest_style__tests__PT027_1.snap | 21 ++ ruff.schema.json | 1 + 9 files changed, 471 insertions(+) create mode 100644 crates/ruff/resources/test/fixtures/flake8_pytest_style/PT027_0.py create mode 100644 crates/ruff/resources/test/fixtures/flake8_pytest_style/PT027_1.py create mode 100644 crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT027_0.snap create mode 100644 crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT027_1.snap diff --git a/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT027_0.py b/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT027_0.py new file mode 100644 index 0000000000000..a12176a1c59b9 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT027_0.py @@ -0,0 +1,45 @@ +import unittest + + +class Test(unittest.TestCase): + def test_errors(self): + with self.assertRaises(ValueError): + raise ValueError + + with self.assertRaises(expected_exception=ValueError): + raise ValueError + + with self.assertRaisesRegex(ValueError, expected_regex="test"): + raise ValueError("test") + + with self.assertRaisesRegex( + expected_exception=ValueError, expected_regex="test" + ): + raise ValueError("test") + + with self.assertRaisesRegex( + expected_regex="test", expected_exception=ValueError + ): + raise ValueError("test") + + with self.failUnlessRaises(ValueError): + raise ValueError + + def test_unfixable_error(self): + with self.assertRaises(ValueError, msg="msg"): + raise ValueError + + with self.assertRaises( + # comment + ValueError + ): + raise ValueError + + with ( + self + # comment + .assertRaises + # comment + (ValueError) + ): + raise ValueError diff --git a/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT027_1.py b/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT027_1.py new file mode 100644 index 0000000000000..708a582ad3d22 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT027_1.py @@ -0,0 +1,12 @@ +import unittest +import pytest + + +class Test(unittest.TestCase): + def test_pytest_raises(self): + with pytest.raises(ValueError): + raise ValueError + + def test_errors(self): + with self.assertRaises(ValueError): + raise ValueError diff --git a/crates/ruff/src/checkers/ast/analyze/expression.rs b/crates/ruff/src/checkers/ast/analyze/expression.rs index d452a718a3aa9..60841d5208649 100644 --- a/crates/ruff/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff/src/checkers/ast/analyze/expression.rs @@ -760,6 +760,13 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { checker.diagnostics.push(diagnostic); } } + if checker.enabled(Rule::PytestUnittestRaisesAssertion) { + if let Some(diagnostic) = flake8_pytest_style::rules::unittest_raises_assertion( + checker, expr, func, args, keywords, + ) { + checker.diagnostics.push(diagnostic); + } + } if checker.enabled(Rule::SubprocessPopenPreexecFn) { pylint::rules::subprocess_popen_preexec_fn(checker, call); } diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index ba86706c6a805..6675fc4373246 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -697,6 +697,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8PytestStyle, "024") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestUnnecessaryAsyncioMarkOnFixture), (Flake8PytestStyle, "025") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestErroneousUseFixturesOnFixture), (Flake8PytestStyle, "026") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestUseFixturesWithoutParameters), + (Flake8PytestStyle, "027") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestUnittestRaisesAssertion), // flake8-pie (Flake8Pie, "790") => (RuleGroup::Unspecified, rules::flake8_pie::rules::UnnecessaryPass), diff --git a/crates/ruff/src/rules/flake8_pytest_style/mod.rs b/crates/ruff/src/rules/flake8_pytest_style/mod.rs index c36222950e15c..023ba3d23eef7 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/mod.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/mod.rs @@ -250,6 +250,18 @@ mod tests { Settings::default(), "PT026" )] + #[test_case( + Rule::PytestUnittestRaisesAssertion, + Path::new("PT027_0.py"), + Settings::default(), + "PT027_0" + )] + #[test_case( + Rule::PytestUnittestRaisesAssertion, + Path::new("PT027_1.py"), + Settings::default(), + "PT027_1" + )] fn test_pytest_style( rule_code: Rule, path: &Path, diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs index dbb571c5272be..5539518da024b 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs @@ -21,6 +21,7 @@ use crate::autofix::codemods::CodegenStylist; use crate::checkers::ast::Checker; use crate::cst::matchers::match_indented_block; use crate::cst::matchers::match_module; +use crate::importer::ImportRequest; use crate::registry::AsRule; use super::unittest_assert::UnittestAssert; @@ -291,6 +292,181 @@ pub(crate) fn unittest_assertion( } } +/// ## What it does +/// Checks for uses of assertion methods for exceptions from the `unittest` module. +/// +/// ## Why is this bad? +/// To enforce the assertion style recommended by `pytest`, `pytest.raises` is +/// preferred. +/// +/// ## Example +/// ```python +/// import unittest +/// +/// +/// class TestFoo(unittest.TestCase): +/// def test_foo(self): +/// with self.assertRaises(ValueError): +/// raise ValueError("foo") +/// ``` +/// +/// Use instead: +/// ```python +/// import unittest +/// import pytest +/// +/// +/// class TestFoo(unittest.TestCase): +/// def test_foo(self): +/// with pytest.raises(ValueError): +/// raise ValueError("foo") +/// ``` +/// +/// ## References +/// - [`pytest` documentation: Assertions about expected exceptions](https://docs.pytest.org/en/latest/how-to/assert.html#assertions-about-expected-exceptions) +#[violation] +pub struct PytestUnittestRaisesAssertion { + assertion: String, +} + +impl Violation for PytestUnittestRaisesAssertion { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + let PytestUnittestRaisesAssertion { assertion } = self; + format!("Use `pytest.raises` instead of unittest-style `{assertion}`") + } + + fn autofix_title(&self) -> Option { + let PytestUnittestRaisesAssertion { assertion } = self; + Some(format!("Replace `{assertion}` with `pytest.raises`")) + } +} + +fn to_pytest_raises_args( + checker: &Checker, + attr: &str, + args: &[Expr], + keywords: &[Keyword], +) -> Result { + let args = match attr { + "assertRaises" | "failUnlessRaises" => match (args, keywords) { + ([arg], []) => { + format!("({})", checker.locator().slice(arg.range())) + } + ([], [arg]) + if arg + .arg + .as_ref() + .map_or(false, |a| a.as_str() == "expected_exception") => + { + format!("({})", checker.locator().slice(arg.value.range())) + } + _ => bail!("Unable to fix"), + }, + "assertRaisesRegex" | "assertRaisesRegexp" => match (args, keywords) { + ([arg1, arg2], []) => { + format!( + "({}, match={})", + checker.locator().slice(arg1.range()), + checker.locator().slice(arg2.range()) + ) + } + ([arg], [kwarg]) + if kwarg + .arg + .as_ref() + .map_or(false, |f| f.as_str() == "expected_regex") => + { + format!( + "({}, match={})", + checker.locator().slice(arg.range()), + checker.locator().slice(kwarg.value.range()) + ) + } + ([], [kwarg1, kwarg2]) + if kwarg1 + .arg + .as_ref() + .map_or(false, |f| f.as_str() == "expected_exception") + && kwarg2 + .arg + .as_ref() + .map_or(false, |f| f.as_str() == "expected_regex") => + { + format!( + "({}, match={})", + checker.locator().slice(kwarg1.value.range()), + checker.locator().slice(kwarg2.value.range()) + ) + } + ([], [kwarg1, kwarg2]) + if kwarg1 + .arg + .as_ref() + .map_or(false, |f| f.as_str() == "expected_regex") + && kwarg2 + .arg + .as_ref() + .map_or(false, |f| f.as_str() == "expected_exception") => + { + format!( + "({}, match={})", + checker.locator().slice(kwarg2.value.range()), + checker.locator().slice(kwarg1.value.range()) + ) + } + _ => bail!("Unable to fix"), + }, + _ => bail!("Unable to fix"), + }; + Ok(args) +} + +/// PT027 +pub(crate) fn unittest_raises_assertion( + checker: &Checker, + expr: &Expr, + func: &Expr, + args: &[Expr], + keywords: &[Keyword], +) -> Option { + let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func else { + return None + }; + + if !matches!( + attr.as_str(), + "assertRaises" | "failUnlessRaises" | "assertRaisesRegex" | "assertRaisesRegexp" + ) { + return None; + } + + let mut diagnostic = Diagnostic::new( + PytestUnittestRaisesAssertion { + assertion: attr.to_string(), + }, + func.range(), + ); + if checker.patch(diagnostic.kind.rule()) + && !checker.indexer().has_comments(expr, checker.locator()) + { + diagnostic.try_set_fix(|| { + let (import_edit, biding) = checker.importer().get_or_import_symbol( + &ImportRequest::import("pytest", "raises"), + func.start(), + checker.semantic(), + )?; + let args = to_pytest_raises_args(checker, attr.as_str(), args, keywords)?; + let edit = Edit::range_replacement(format!("{biding}{args}"), expr.range()); + Ok(Fix::suggested_edits(import_edit, [edit])) + }); + } + + Some(diagnostic) +} + /// PT015 pub(crate) fn assert_falsy(checker: &mut Checker, stmt: &Stmt, test: &Expr) { if Truthiness::from_expr(test, |id| checker.semantic().is_builtin(id)).is_falsey() { diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT027_0.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT027_0.snap new file mode 100644 index 0000000000000..a33d1801dad94 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT027_0.snap @@ -0,0 +1,196 @@ +--- +source: crates/ruff/src/rules/flake8_pytest_style/mod.rs +--- +PT027_0.py:6:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaises` + | +4 | class Test(unittest.TestCase): +5 | def test_errors(self): +6 | with self.assertRaises(ValueError): + | ^^^^^^^^^^^^^^^^^ PT027 +7 | raise ValueError + | + = help: Replace `assertRaises` with `pytest.raises` + +ℹ Suggested fix +1 1 | import unittest + 2 |+import pytest +2 3 | +3 4 | +4 5 | class Test(unittest.TestCase): +5 6 | def test_errors(self): +6 |- with self.assertRaises(ValueError): + 7 |+ with pytest.raises(ValueError): +7 8 | raise ValueError +8 9 | +9 10 | with self.assertRaises(expected_exception=ValueError): + +PT027_0.py:9:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaises` + | + 7 | raise ValueError + 8 | + 9 | with self.assertRaises(expected_exception=ValueError): + | ^^^^^^^^^^^^^^^^^ PT027 +10 | raise ValueError + | + = help: Replace `assertRaises` with `pytest.raises` + +ℹ Suggested fix +1 1 | import unittest + 2 |+import pytest +2 3 | +3 4 | +4 5 | class Test(unittest.TestCase): +-------------------------------------------------------------------------------- +6 7 | with self.assertRaises(ValueError): +7 8 | raise ValueError +8 9 | +9 |- with self.assertRaises(expected_exception=ValueError): + 10 |+ with pytest.raises(ValueError): +10 11 | raise ValueError +11 12 | +12 13 | with self.assertRaisesRegex(ValueError, expected_regex="test"): + +PT027_0.py:12:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaisesRegex` + | +10 | raise ValueError +11 | +12 | with self.assertRaisesRegex(ValueError, expected_regex="test"): + | ^^^^^^^^^^^^^^^^^^^^^^ PT027 +13 | raise ValueError("test") + | + = help: Replace `assertRaisesRegex` with `pytest.raises` + +ℹ Suggested fix +1 1 | import unittest + 2 |+import pytest +2 3 | +3 4 | +4 5 | class Test(unittest.TestCase): +-------------------------------------------------------------------------------- +9 10 | with self.assertRaises(expected_exception=ValueError): +10 11 | raise ValueError +11 12 | +12 |- with self.assertRaisesRegex(ValueError, expected_regex="test"): + 13 |+ with pytest.raises(ValueError, match="test"): +13 14 | raise ValueError("test") +14 15 | +15 16 | with self.assertRaisesRegex( + +PT027_0.py:15:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaisesRegex` + | +13 | raise ValueError("test") +14 | +15 | with self.assertRaisesRegex( + | ^^^^^^^^^^^^^^^^^^^^^^ PT027 +16 | expected_exception=ValueError, expected_regex="test" +17 | ): + | + = help: Replace `assertRaisesRegex` with `pytest.raises` + +ℹ Suggested fix +1 1 | import unittest + 2 |+import pytest +2 3 | +3 4 | +4 5 | class Test(unittest.TestCase): +-------------------------------------------------------------------------------- +12 13 | with self.assertRaisesRegex(ValueError, expected_regex="test"): +13 14 | raise ValueError("test") +14 15 | +15 |- with self.assertRaisesRegex( +16 |- expected_exception=ValueError, expected_regex="test" +17 |- ): + 16 |+ with pytest.raises(ValueError, match="test"): +18 17 | raise ValueError("test") +19 18 | +20 19 | with self.assertRaisesRegex( + +PT027_0.py:20:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaisesRegex` + | +18 | raise ValueError("test") +19 | +20 | with self.assertRaisesRegex( + | ^^^^^^^^^^^^^^^^^^^^^^ PT027 +21 | expected_regex="test", expected_exception=ValueError +22 | ): + | + = help: Replace `assertRaisesRegex` with `pytest.raises` + +ℹ Suggested fix +1 1 | import unittest + 2 |+import pytest +2 3 | +3 4 | +4 5 | class Test(unittest.TestCase): +-------------------------------------------------------------------------------- +17 18 | ): +18 19 | raise ValueError("test") +19 20 | +20 |- with self.assertRaisesRegex( +21 |- expected_regex="test", expected_exception=ValueError +22 |- ): + 21 |+ with pytest.raises(ValueError, match="test"): +23 22 | raise ValueError("test") +24 23 | +25 24 | with self.failUnlessRaises(ValueError): + +PT027_0.py:25:14: PT027 [*] Use `pytest.raises` instead of unittest-style `failUnlessRaises` + | +23 | raise ValueError("test") +24 | +25 | with self.failUnlessRaises(ValueError): + | ^^^^^^^^^^^^^^^^^^^^^ PT027 +26 | raise ValueError + | + = help: Replace `failUnlessRaises` with `pytest.raises` + +ℹ Suggested fix +1 1 | import unittest + 2 |+import pytest +2 3 | +3 4 | +4 5 | class Test(unittest.TestCase): +-------------------------------------------------------------------------------- +22 23 | ): +23 24 | raise ValueError("test") +24 25 | +25 |- with self.failUnlessRaises(ValueError): + 26 |+ with pytest.raises(ValueError): +26 27 | raise ValueError +27 28 | +28 29 | def test_unfixable_error(self): + +PT027_0.py:29:14: PT027 Use `pytest.raises` instead of unittest-style `assertRaises` + | +28 | def test_unfixable_error(self): +29 | with self.assertRaises(ValueError, msg="msg"): + | ^^^^^^^^^^^^^^^^^ PT027 +30 | raise ValueError + | + = help: Replace `assertRaises` with `pytest.raises` + +PT027_0.py:32:14: PT027 Use `pytest.raises` instead of unittest-style `assertRaises` + | +30 | raise ValueError +31 | +32 | with self.assertRaises( + | ^^^^^^^^^^^^^^^^^ PT027 +33 | # comment +34 | ValueError + | + = help: Replace `assertRaises` with `pytest.raises` + +PT027_0.py:39:13: PT027 Use `pytest.raises` instead of unittest-style `assertRaises` + | +38 | with ( +39 | self + | _____________^ +40 | | # comment +41 | | .assertRaises + | |_________________________^ PT027 +42 | # comment +43 | (ValueError) + | + = help: Replace `assertRaises` with `pytest.raises` + + diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT027_1.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT027_1.snap new file mode 100644 index 0000000000000..7656398481c0d --- /dev/null +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT027_1.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff/src/rules/flake8_pytest_style/mod.rs +--- +PT027_1.py:11:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaises` + | +10 | def test_errors(self): +11 | with self.assertRaises(ValueError): + | ^^^^^^^^^^^^^^^^^ PT027 +12 | raise ValueError + | + = help: Replace `assertRaises` with `pytest.raises` + +ℹ Suggested fix +8 8 | raise ValueError +9 9 | +10 10 | def test_errors(self): +11 |- with self.assertRaises(ValueError): + 11 |+ with pytest.raises(ValueError): +12 12 | raise ValueError + + diff --git a/ruff.schema.json b/ruff.schema.json index 95cda80152462..b42b038d5294c 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2335,6 +2335,7 @@ "PT024", "PT025", "PT026", + "PT027", "PTH", "PTH1", "PTH10", From 00362f98b45547952007258b7ae265ae0c38ce53 Mon Sep 17 00:00:00 2001 From: harupy Date: Mon, 14 Aug 2023 20:32:48 +0900 Subject: [PATCH 2/6] Fix tests --- .../fixtures/flake8_pytest_style/PT027_0.py | 15 +- ...__flake8_pytest_style__tests__PT027_0.snap | 214 +++++++++++------- 2 files changed, 142 insertions(+), 87 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT027_0.py b/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT027_0.py index a12176a1c59b9..23678ce849b27 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT027_0.py +++ b/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT027_0.py @@ -5,10 +5,15 @@ class Test(unittest.TestCase): def test_errors(self): with self.assertRaises(ValueError): raise ValueError - with self.assertRaises(expected_exception=ValueError): raise ValueError + with self.failUnlessRaises(ValueError): + raise ValueError + + with self.assertRaisesRegex(ValueError, "test"): + raise ValueError("test") + with self.assertRaisesRegex(ValueError, expected_regex="test"): raise ValueError("test") @@ -22,8 +27,8 @@ def test_errors(self): ): raise ValueError("test") - with self.failUnlessRaises(ValueError): - raise ValueError + with self.assertRaisesRegexp(ValueError, "test"): + raise ValueError("test") def test_unfixable_error(self): with self.assertRaises(ValueError, msg="msg"): @@ -38,8 +43,6 @@ def test_unfixable_error(self): with ( self # comment - .assertRaises - # comment - (ValueError) + .assertRaises(ValueError) ): raise ValueError diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT027_0.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT027_0.snap index a33d1801dad94..5e0cbcd04956f 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT027_0.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT027_0.snap @@ -8,6 +8,7 @@ PT027_0.py:6:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assert 6 | with self.assertRaises(ValueError): | ^^^^^^^^^^^^^^^^^ PT027 7 | raise ValueError +8 | with self.assertRaises(expected_exception=ValueError): | = help: Replace `assertRaises` with `pytest.raises` @@ -21,18 +22,43 @@ PT027_0.py:6:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assert 6 |- with self.assertRaises(ValueError): 7 |+ with pytest.raises(ValueError): 7 8 | raise ValueError -8 9 | -9 10 | with self.assertRaises(expected_exception=ValueError): +8 9 | with self.assertRaises(expected_exception=ValueError): +9 10 | raise ValueError -PT027_0.py:9:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaises` +PT027_0.py:8:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaises` + | +6 | with self.assertRaises(ValueError): +7 | raise ValueError +8 | with self.assertRaises(expected_exception=ValueError): + | ^^^^^^^^^^^^^^^^^ PT027 +9 | raise ValueError + | + = help: Replace `assertRaises` with `pytest.raises` + +ℹ Suggested fix +1 1 | import unittest + 2 |+import pytest +2 3 | +3 4 | +4 5 | class Test(unittest.TestCase): +5 6 | def test_errors(self): +6 7 | with self.assertRaises(ValueError): +7 8 | raise ValueError +8 |- with self.assertRaises(expected_exception=ValueError): + 9 |+ with pytest.raises(ValueError): +9 10 | raise ValueError +10 11 | +11 12 | with self.failUnlessRaises(ValueError): + +PT027_0.py:11:14: PT027 [*] Use `pytest.raises` instead of unittest-style `failUnlessRaises` | - 7 | raise ValueError - 8 | - 9 | with self.assertRaises(expected_exception=ValueError): - | ^^^^^^^^^^^^^^^^^ PT027 -10 | raise ValueError + 9 | raise ValueError +10 | +11 | with self.failUnlessRaises(ValueError): + | ^^^^^^^^^^^^^^^^^^^^^ PT027 +12 | raise ValueError | - = help: Replace `assertRaises` with `pytest.raises` + = help: Replace `failUnlessRaises` with `pytest.raises` ℹ Suggested fix 1 1 | import unittest @@ -41,22 +67,22 @@ PT027_0.py:9:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assert 3 4 | 4 5 | class Test(unittest.TestCase): -------------------------------------------------------------------------------- -6 7 | with self.assertRaises(ValueError): -7 8 | raise ValueError -8 9 | -9 |- with self.assertRaises(expected_exception=ValueError): - 10 |+ with pytest.raises(ValueError): -10 11 | raise ValueError -11 12 | -12 13 | with self.assertRaisesRegex(ValueError, expected_regex="test"): - -PT027_0.py:12:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaisesRegex` - | -10 | raise ValueError -11 | -12 | with self.assertRaisesRegex(ValueError, expected_regex="test"): +8 9 | with self.assertRaises(expected_exception=ValueError): +9 10 | raise ValueError +10 11 | +11 |- with self.failUnlessRaises(ValueError): + 12 |+ with pytest.raises(ValueError): +12 13 | raise ValueError +13 14 | +14 15 | with self.assertRaisesRegex(ValueError, "test"): + +PT027_0.py:14:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaisesRegex` + | +12 | raise ValueError +13 | +14 | with self.assertRaisesRegex(ValueError, "test"): | ^^^^^^^^^^^^^^^^^^^^^^ PT027 -13 | raise ValueError("test") +15 | raise ValueError("test") | = help: Replace `assertRaisesRegex` with `pytest.raises` @@ -67,23 +93,22 @@ PT027_0.py:12:14: PT027 [*] Use `pytest.raises` instead of unittest-style `asser 3 4 | 4 5 | class Test(unittest.TestCase): -------------------------------------------------------------------------------- -9 10 | with self.assertRaises(expected_exception=ValueError): -10 11 | raise ValueError -11 12 | -12 |- with self.assertRaisesRegex(ValueError, expected_regex="test"): - 13 |+ with pytest.raises(ValueError, match="test"): -13 14 | raise ValueError("test") -14 15 | -15 16 | with self.assertRaisesRegex( - -PT027_0.py:15:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaisesRegex` - | -13 | raise ValueError("test") -14 | -15 | with self.assertRaisesRegex( +11 12 | with self.failUnlessRaises(ValueError): +12 13 | raise ValueError +13 14 | +14 |- with self.assertRaisesRegex(ValueError, "test"): + 15 |+ with pytest.raises(ValueError, match="test"): +15 16 | raise ValueError("test") +16 17 | +17 18 | with self.assertRaisesRegex(ValueError, expected_regex="test"): + +PT027_0.py:17:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaisesRegex` + | +15 | raise ValueError("test") +16 | +17 | with self.assertRaisesRegex(ValueError, expected_regex="test"): | ^^^^^^^^^^^^^^^^^^^^^^ PT027 -16 | expected_exception=ValueError, expected_regex="test" -17 | ): +18 | raise ValueError("test") | = help: Replace `assertRaisesRegex` with `pytest.raises` @@ -94,16 +119,14 @@ PT027_0.py:15:14: PT027 [*] Use `pytest.raises` instead of unittest-style `asser 3 4 | 4 5 | class Test(unittest.TestCase): -------------------------------------------------------------------------------- -12 13 | with self.assertRaisesRegex(ValueError, expected_regex="test"): -13 14 | raise ValueError("test") -14 15 | -15 |- with self.assertRaisesRegex( -16 |- expected_exception=ValueError, expected_regex="test" -17 |- ): - 16 |+ with pytest.raises(ValueError, match="test"): -18 17 | raise ValueError("test") -19 18 | -20 19 | with self.assertRaisesRegex( +14 15 | with self.assertRaisesRegex(ValueError, "test"): +15 16 | raise ValueError("test") +16 17 | +17 |- with self.assertRaisesRegex(ValueError, expected_regex="test"): + 18 |+ with pytest.raises(ValueError, match="test"): +18 19 | raise ValueError("test") +19 20 | +20 21 | with self.assertRaisesRegex( PT027_0.py:20:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaisesRegex` | @@ -111,7 +134,7 @@ PT027_0.py:20:14: PT027 [*] Use `pytest.raises` instead of unittest-style `asser 19 | 20 | with self.assertRaisesRegex( | ^^^^^^^^^^^^^^^^^^^^^^ PT027 -21 | expected_regex="test", expected_exception=ValueError +21 | expected_exception=ValueError, expected_regex="test" 22 | ): | = help: Replace `assertRaisesRegex` with `pytest.raises` @@ -123,26 +146,27 @@ PT027_0.py:20:14: PT027 [*] Use `pytest.raises` instead of unittest-style `asser 3 4 | 4 5 | class Test(unittest.TestCase): -------------------------------------------------------------------------------- -17 18 | ): +17 18 | with self.assertRaisesRegex(ValueError, expected_regex="test"): 18 19 | raise ValueError("test") 19 20 | 20 |- with self.assertRaisesRegex( -21 |- expected_regex="test", expected_exception=ValueError +21 |- expected_exception=ValueError, expected_regex="test" 22 |- ): 21 |+ with pytest.raises(ValueError, match="test"): 23 22 | raise ValueError("test") 24 23 | -25 24 | with self.failUnlessRaises(ValueError): +25 24 | with self.assertRaisesRegex( -PT027_0.py:25:14: PT027 [*] Use `pytest.raises` instead of unittest-style `failUnlessRaises` +PT027_0.py:25:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaisesRegex` | 23 | raise ValueError("test") 24 | -25 | with self.failUnlessRaises(ValueError): - | ^^^^^^^^^^^^^^^^^^^^^ PT027 -26 | raise ValueError +25 | with self.assertRaisesRegex( + | ^^^^^^^^^^^^^^^^^^^^^^ PT027 +26 | expected_regex="test", expected_exception=ValueError +27 | ): | - = help: Replace `failUnlessRaises` with `pytest.raises` + = help: Replace `assertRaisesRegex` with `pytest.raises` ℹ Suggested fix 1 1 | import unittest @@ -154,42 +178,70 @@ PT027_0.py:25:14: PT027 [*] Use `pytest.raises` instead of unittest-style `failU 22 23 | ): 23 24 | raise ValueError("test") 24 25 | -25 |- with self.failUnlessRaises(ValueError): - 26 |+ with pytest.raises(ValueError): -26 27 | raise ValueError -27 28 | -28 29 | def test_unfixable_error(self): +25 |- with self.assertRaisesRegex( +26 |- expected_regex="test", expected_exception=ValueError +27 |- ): + 26 |+ with pytest.raises(ValueError, match="test"): +28 27 | raise ValueError("test") +29 28 | +30 29 | with self.assertRaisesRegexp(ValueError, "test"): -PT027_0.py:29:14: PT027 Use `pytest.raises` instead of unittest-style `assertRaises` +PT027_0.py:30:14: PT027 [*] Use `pytest.raises` instead of unittest-style `assertRaisesRegexp` | -28 | def test_unfixable_error(self): -29 | with self.assertRaises(ValueError, msg="msg"): +28 | raise ValueError("test") +29 | +30 | with self.assertRaisesRegexp(ValueError, "test"): + | ^^^^^^^^^^^^^^^^^^^^^^^ PT027 +31 | raise ValueError("test") + | + = help: Replace `assertRaisesRegexp` with `pytest.raises` + +ℹ Suggested fix +1 1 | import unittest + 2 |+import pytest +2 3 | +3 4 | +4 5 | class Test(unittest.TestCase): +-------------------------------------------------------------------------------- +27 28 | ): +28 29 | raise ValueError("test") +29 30 | +30 |- with self.assertRaisesRegexp(ValueError, "test"): + 31 |+ with pytest.raises(ValueError, match="test"): +31 32 | raise ValueError("test") +32 33 | +33 34 | def test_unfixable_error(self): + +PT027_0.py:34:14: PT027 Use `pytest.raises` instead of unittest-style `assertRaises` + | +33 | def test_unfixable_error(self): +34 | with self.assertRaises(ValueError, msg="msg"): | ^^^^^^^^^^^^^^^^^ PT027 -30 | raise ValueError +35 | raise ValueError | = help: Replace `assertRaises` with `pytest.raises` -PT027_0.py:32:14: PT027 Use `pytest.raises` instead of unittest-style `assertRaises` +PT027_0.py:37:14: PT027 Use `pytest.raises` instead of unittest-style `assertRaises` | -30 | raise ValueError -31 | -32 | with self.assertRaises( +35 | raise ValueError +36 | +37 | with self.assertRaises( | ^^^^^^^^^^^^^^^^^ PT027 -33 | # comment -34 | ValueError +38 | # comment +39 | ValueError | = help: Replace `assertRaises` with `pytest.raises` -PT027_0.py:39:13: PT027 Use `pytest.raises` instead of unittest-style `assertRaises` +PT027_0.py:44:13: PT027 Use `pytest.raises` instead of unittest-style `assertRaises` | -38 | with ( -39 | self +43 | with ( +44 | self | _____________^ -40 | | # comment -41 | | .assertRaises +45 | | # comment +46 | | .assertRaises(ValueError) | |_________________________^ PT027 -42 | # comment -43 | (ValueError) +47 | ): +48 | raise ValueError | = help: Replace `assertRaises` with `pytest.raises` From 42b32c35cd67a77452d1d5936f83f8fa049cb08f Mon Sep 17 00:00:00 2001 From: harupy Date: Mon, 14 Aug 2023 20:34:32 +0900 Subject: [PATCH 3/6] Rename --- .../ruff/src/rules/flake8_pytest_style/rules/assertion.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs index 5539518da024b..2419e9383645c 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs @@ -355,13 +355,13 @@ fn to_pytest_raises_args( ([arg], []) => { format!("({})", checker.locator().slice(arg.range())) } - ([], [arg]) - if arg + ([], [kwarg]) + if kwarg .arg .as_ref() .map_or(false, |a| a.as_str() == "expected_exception") => { - format!("({})", checker.locator().slice(arg.value.range())) + format!("({})", checker.locator().slice(kwarg.value.range())) } _ => bail!("Unable to fix"), }, From 41d57279bb8acbc2dfc0db3f68e1e02882775ed3 Mon Sep 17 00:00:00 2001 From: harupy Date: Mon, 14 Aug 2023 20:41:29 +0900 Subject: [PATCH 4/6] Comments --- .../ruff/src/rules/flake8_pytest_style/rules/assertion.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs index 2419e9383645c..b68a7c04d465f 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs @@ -352,9 +352,11 @@ fn to_pytest_raises_args( ) -> Result { let args = match attr { "assertRaises" | "failUnlessRaises" => match (args, keywords) { + // assertRaises(Exception) ([arg], []) => { format!("({})", checker.locator().slice(arg.range())) } + // assertRaises(expected_exception=Exception) ([], [kwarg]) if kwarg .arg @@ -366,6 +368,7 @@ fn to_pytest_raises_args( _ => bail!("Unable to fix"), }, "assertRaisesRegex" | "assertRaisesRegexp" => match (args, keywords) { + // assertRaisesRegex(Exception, regex) ([arg1, arg2], []) => { format!( "({}, match={})", @@ -373,6 +376,7 @@ fn to_pytest_raises_args( checker.locator().slice(arg2.range()) ) } + // assertRaisesRegex(Exception, expected_regex=regex) ([arg], [kwarg]) if kwarg .arg @@ -385,6 +389,7 @@ fn to_pytest_raises_args( checker.locator().slice(kwarg.value.range()) ) } + // assertRaisesRegex(expected_exception=Exception, expected_regex=regex) ([], [kwarg1, kwarg2]) if kwarg1 .arg @@ -401,6 +406,7 @@ fn to_pytest_raises_args( checker.locator().slice(kwarg2.value.range()) ) } + // assertRaisesRegex(expected_regex=regex, expected_exception=Exception) ([], [kwarg1, kwarg2]) if kwarg1 .arg From 81df0846e0a38f5c5880870be3379f2d8ef6321a Mon Sep 17 00:00:00 2001 From: harupy Date: Mon, 14 Aug 2023 20:48:23 +0900 Subject: [PATCH 5/6] Rename method --- .../resources/test/fixtures/flake8_pytest_style/PT027_0.py | 2 +- .../ruff__rules__flake8_pytest_style__tests__PT027_0.snap | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT027_0.py b/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT027_0.py index 23678ce849b27..b9614f0647dc1 100644 --- a/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT027_0.py +++ b/crates/ruff/resources/test/fixtures/flake8_pytest_style/PT027_0.py @@ -30,7 +30,7 @@ def test_errors(self): with self.assertRaisesRegexp(ValueError, "test"): raise ValueError("test") - def test_unfixable_error(self): + def test_unfixable_errors(self): with self.assertRaises(ValueError, msg="msg"): raise ValueError diff --git a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT027_0.snap b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT027_0.snap index 5e0cbcd04956f..8ac1da881c437 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT027_0.snap +++ b/crates/ruff/src/rules/flake8_pytest_style/snapshots/ruff__rules__flake8_pytest_style__tests__PT027_0.snap @@ -210,11 +210,11 @@ PT027_0.py:30:14: PT027 [*] Use `pytest.raises` instead of unittest-style `asser 31 |+ with pytest.raises(ValueError, match="test"): 31 32 | raise ValueError("test") 32 33 | -33 34 | def test_unfixable_error(self): +33 34 | def test_unfixable_errors(self): PT027_0.py:34:14: PT027 Use `pytest.raises` instead of unittest-style `assertRaises` | -33 | def test_unfixable_error(self): +33 | def test_unfixable_errors(self): 34 | with self.assertRaises(ValueError, msg="msg"): | ^^^^^^^^^^^^^^^^^ PT027 35 | raise ValueError From cba3295ef8b6765a9e53408f2a972d74e44a730e Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 14 Aug 2023 16:14:54 -0400 Subject: [PATCH 6/6] Tweak signature --- .../src/checkers/ast/analyze/expression.rs | 6 +- .../flake8_pytest_style/rules/assertion.rs | 215 +++++++++--------- .../tryceratops/rules/raise_vanilla_args.rs | 3 +- 3 files changed, 113 insertions(+), 111 deletions(-) diff --git a/crates/ruff/src/checkers/ast/analyze/expression.rs b/crates/ruff/src/checkers/ast/analyze/expression.rs index 125507917d910..2951899f4c8b0 100644 --- a/crates/ruff/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff/src/checkers/ast/analyze/expression.rs @@ -757,9 +757,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) { } } if checker.enabled(Rule::PytestUnittestRaisesAssertion) { - if let Some(diagnostic) = flake8_pytest_style::rules::unittest_raises_assertion( - checker, expr, func, args, keywords, - ) { + if let Some(diagnostic) = + flake8_pytest_style::rules::unittest_raises_assertion(checker, call) + { checker.diagnostics.push(diagnostic); } } diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs index b68a7c04d465f..5514cd8744a03 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs @@ -7,12 +7,14 @@ use libcst_native::{ ParenthesizedNode, SimpleStatementLine, SimpleWhitespace, SmallStatement, Statement, TrailingWhitespace, UnaryOperation, }; -use ruff_python_ast::{self as ast, BoolOp, ExceptHandler, Expr, Keyword, Ranged, Stmt, UnaryOp}; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::Truthiness; use ruff_python_ast::visitor::Visitor; +use ruff_python_ast::{ + self as ast, Arguments, BoolOp, ExceptHandler, Expr, Keyword, Ranged, Stmt, UnaryOp, +}; use ruff_python_ast::{visitor, whitespace}; use ruff_python_codegen::Stylist; use ruff_source_file::Locator; @@ -293,11 +295,13 @@ pub(crate) fn unittest_assertion( } /// ## What it does -/// Checks for uses of assertion methods for exceptions from the `unittest` module. +/// Checks for uses of exception-related assertion methods from the `unittest` +/// module. /// /// ## Why is this bad? /// To enforce the assertion style recommended by `pytest`, `pytest.raises` is -/// preferred. +/// preferred over the exception-related assertion methods in `unittest`, like +/// `assertRaises`. /// /// ## Example /// ```python @@ -344,102 +348,13 @@ impl Violation for PytestUnittestRaisesAssertion { } } -fn to_pytest_raises_args( - checker: &Checker, - attr: &str, - args: &[Expr], - keywords: &[Keyword], -) -> Result { - let args = match attr { - "assertRaises" | "failUnlessRaises" => match (args, keywords) { - // assertRaises(Exception) - ([arg], []) => { - format!("({})", checker.locator().slice(arg.range())) - } - // assertRaises(expected_exception=Exception) - ([], [kwarg]) - if kwarg - .arg - .as_ref() - .map_or(false, |a| a.as_str() == "expected_exception") => - { - format!("({})", checker.locator().slice(kwarg.value.range())) - } - _ => bail!("Unable to fix"), - }, - "assertRaisesRegex" | "assertRaisesRegexp" => match (args, keywords) { - // assertRaisesRegex(Exception, regex) - ([arg1, arg2], []) => { - format!( - "({}, match={})", - checker.locator().slice(arg1.range()), - checker.locator().slice(arg2.range()) - ) - } - // assertRaisesRegex(Exception, expected_regex=regex) - ([arg], [kwarg]) - if kwarg - .arg - .as_ref() - .map_or(false, |f| f.as_str() == "expected_regex") => - { - format!( - "({}, match={})", - checker.locator().slice(arg.range()), - checker.locator().slice(kwarg.value.range()) - ) - } - // assertRaisesRegex(expected_exception=Exception, expected_regex=regex) - ([], [kwarg1, kwarg2]) - if kwarg1 - .arg - .as_ref() - .map_or(false, |f| f.as_str() == "expected_exception") - && kwarg2 - .arg - .as_ref() - .map_or(false, |f| f.as_str() == "expected_regex") => - { - format!( - "({}, match={})", - checker.locator().slice(kwarg1.value.range()), - checker.locator().slice(kwarg2.value.range()) - ) - } - // assertRaisesRegex(expected_regex=regex, expected_exception=Exception) - ([], [kwarg1, kwarg2]) - if kwarg1 - .arg - .as_ref() - .map_or(false, |f| f.as_str() == "expected_regex") - && kwarg2 - .arg - .as_ref() - .map_or(false, |f| f.as_str() == "expected_exception") => - { - format!( - "({}, match={})", - checker.locator().slice(kwarg2.value.range()), - checker.locator().slice(kwarg1.value.range()) - ) - } - _ => bail!("Unable to fix"), - }, - _ => bail!("Unable to fix"), - }; - Ok(args) -} - /// PT027 pub(crate) fn unittest_raises_assertion( checker: &Checker, - expr: &Expr, - func: &Expr, - args: &[Expr], - keywords: &[Keyword], + call: &ast::ExprCall, ) -> Option { - let Expr::Attribute(ast::ExprAttribute { attr, .. }) = func else { - return None + let Expr::Attribute(ast::ExprAttribute { attr, .. }) = call.func.as_ref() else { + return None; }; if !matches!( @@ -453,26 +368,112 @@ pub(crate) fn unittest_raises_assertion( PytestUnittestRaisesAssertion { assertion: attr.to_string(), }, - func.range(), + call.func.range(), ); if checker.patch(diagnostic.kind.rule()) - && !checker.indexer().has_comments(expr, checker.locator()) + && !checker.indexer().has_comments(call, checker.locator()) { - diagnostic.try_set_fix(|| { - let (import_edit, biding) = checker.importer().get_or_import_symbol( - &ImportRequest::import("pytest", "raises"), - func.start(), - checker.semantic(), - )?; - let args = to_pytest_raises_args(checker, attr.as_str(), args, keywords)?; - let edit = Edit::range_replacement(format!("{biding}{args}"), expr.range()); - Ok(Fix::suggested_edits(import_edit, [edit])) - }); + if let Some(args) = to_pytest_raises_args(checker, attr.as_str(), &call.arguments) { + diagnostic.try_set_fix(|| { + let (import_edit, binding) = checker.importer().get_or_import_symbol( + &ImportRequest::import("pytest", "raises"), + call.func.start(), + checker.semantic(), + )?; + let edit = Edit::range_replacement(format!("{binding}({args})"), call.range()); + Ok(Fix::suggested_edits(import_edit, [edit])) + }); + } } Some(diagnostic) } +fn to_pytest_raises_args<'a>( + checker: &Checker<'a>, + attr: &str, + arguments: &Arguments, +) -> Option> { + let args = match attr { + "assertRaises" | "failUnlessRaises" => { + match (arguments.args.as_slice(), arguments.keywords.as_slice()) { + // Ex) `assertRaises(Exception)` + ([arg], []) => Cow::Borrowed(checker.locator().slice(arg.range())), + // Ex) `assertRaises(expected_exception=Exception)` + ([], [kwarg]) + if kwarg + .arg + .as_ref() + .is_some_and(|id| id.as_str() == "expected_exception") => + { + Cow::Borrowed(checker.locator().slice(kwarg.value.range())) + } + _ => return None, + } + } + "assertRaisesRegex" | "assertRaisesRegexp" => { + match (arguments.args.as_slice(), arguments.keywords.as_slice()) { + // Ex) `assertRaisesRegex(Exception, regex)` + ([arg1, arg2], []) => Cow::Owned(format!( + "{}, match={}", + checker.locator().slice(arg1.range()), + checker.locator().slice(arg2.range()) + )), + // Ex) `assertRaisesRegex(Exception, expected_regex=regex)` + ([arg], [kwarg]) + if kwarg + .arg + .as_ref() + .is_some_and(|arg| arg.as_str() == "expected_regex") => + { + Cow::Owned(format!( + "{}, match={}", + checker.locator().slice(arg.range()), + checker.locator().slice(kwarg.value.range()) + )) + } + // Ex) `assertRaisesRegex(expected_exception=Exception, expected_regex=regex)` + ([], [kwarg1, kwarg2]) + if kwarg1 + .arg + .as_ref() + .is_some_and(|id| id.as_str() == "expected_exception") + && kwarg2 + .arg + .as_ref() + .is_some_and(|id| id.as_str() == "expected_regex") => + { + Cow::Owned(format!( + "{}, match={}", + checker.locator().slice(kwarg1.value.range()), + checker.locator().slice(kwarg2.value.range()) + )) + } + // Ex) `assertRaisesRegex(expected_regex=regex, expected_exception=Exception)` + ([], [kwarg1, kwarg2]) + if kwarg1 + .arg + .as_ref() + .is_some_and(|id| id.as_str() == "expected_regex") + && kwarg2 + .arg + .as_ref() + .is_some_and(|id| id.as_str() == "expected_exception") => + { + Cow::Owned(format!( + "{}, match={}", + checker.locator().slice(kwarg2.value.range()), + checker.locator().slice(kwarg1.value.range()) + )) + } + _ => return None, + } + } + _ => return None, + }; + Some(args) +} + /// PT015 pub(crate) fn assert_falsy(checker: &mut Checker, stmt: &Stmt, test: &Expr) { if Truthiness::from_expr(test, |id| checker.semantic().is_builtin(id)).is_falsey() { diff --git a/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_args.rs b/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_args.rs index d9cc91351b6c4..aadfa930c49f7 100644 --- a/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_args.rs +++ b/crates/ruff/src/rules/tryceratops/rules/raise_vanilla_args.rs @@ -58,7 +58,8 @@ pub(crate) fn raise_vanilla_args(checker: &mut Checker, expr: &Expr) { func, arguments: Arguments { args, .. }, .. - }) = expr else { + }) = expr + else { return; };