diff --git a/README.md b/README.md index b23b6d4c64..f39a766c8d 100644 --- a/README.md +++ b/README.md @@ -919,7 +919,7 @@ For more, see [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style | PT006 | ParametrizeNamesWrongType | Wrong name(s) type in `@pytest.mark.parametrize`, expected `tuple` | 🛠 | | PT007 | ParametrizeValuesWrongType | Wrong values type in `@pytest.mark.parametrize` expected `list` of `tuple` | | | PT008 | PatchWithLambda | Use `return_value=` instead of patching with `lambda` | | -| PT009 | UnittestAssertion | Use a regular `assert` instead of unittest-style `...` | | +| PT009 | UnittestAssertion | Use a regular `assert` instead of unittest-style `...` | 🛠 | | PT010 | RaisesWithoutException | set the expected exception in `pytest.raises()` | | | PT011 | RaisesTooBroad | `pytest.raises(...)` is too broad, set the `match` parameter or use a more specific exception | | | PT012 | RaisesWithMultipleStatements | `pytest.raises()` block should contain a single simple statement | | diff --git a/resources/test/fixtures/flake8_pytest_style/PT009.py b/resources/test/fixtures/flake8_pytest_style/PT009.py index 0b02f989aa..e99d8e06a8 100644 --- a/resources/test/fixtures/flake8_pytest_style/PT009.py +++ b/resources/test/fixtures/flake8_pytest_style/PT009.py @@ -1,9 +1,76 @@ -import pytest +import unittest -def test_xxx(): - assert 1 == 1 # OK no parameters +class Test(unittest.TestCase): + def test_xxx(self): + assert 1 == 1 # OK no parameters + def test_assert_true(self): + expr = 1 + msg = "Must be True" + self.assertTrue(expr) # Error + self.assertTrue(expr=expr) # Error + self.assertTrue(expr, msg) # Error + self.assertTrue(expr=expr, msg=msg) # Error + self.assertTrue(msg=msg, expr=expr) # Error + self.assertTrue(*(expr, msg)) # Error, unfixable + self.assertTrue(**{"expr": expr, "msg": msg}) # Error, unfixable + self.assertTrue(msg=msg, expr=expr, unexpected_arg=False) # Error, unfixable + self.assertTrue(msg=msg) # Error, unfixable -def test_xxx(): - self.assertEqual(1, 1) # Error + def test_assert_false(self): + self.assertFalse(True) # Error + + def test_assert_equal(self): + self.assertEqual(1, 2) # Error + + def test_assert_not_equal(self): + self.assertNotEqual(1, 1) # Error + + def test_assert_greater(self): + self.assertGreater(1, 2) # Error + + def test_assert_greater_equal(self): + self.assertGreaterEqual(1, 2) # Error + + def test_assert_less(self): + self.assertLess(2, 1) # Error + + def test_assert_less_equal(self): + self.assertLessEqual(1, 2) # Error + + def test_assert_in(self): + self.assertIn(1, [2, 3]) # Error + + def test_assert_not_in(self): + self.assertNotIn(2, [2, 3]) # Error + + def test_assert_is_none(self): + self.assertIsNone(0) # Error + + def test_assert_is_not_none(self): + self.assertIsNotNone(0) # Error + + def test_assert_is(self): + self.assertIs([], []) # Error + + def test_assert_is_not(self): + self.assertIsNot(1, 1) # Error + + def test_assert_is_instance(self): + self.assertIsInstance(1, str) # Error + + def test_assert_is_not_instance(self): + self.assertNotIsInstance(1, int) # Error + + def test_assert_regex(self): + self.assertRegex("abc", r"def") # Error + + def test_assert_not_regex(self): + self.assertNotRegex("abc", r"abc") # Error + + def test_assert_regexp_matches(self): + self.assertRegexpMatches("abc", r"def") # Error + + def test_assert_not_regexp_matches(self): + self.assertNotRegex("abc", r"abc") # Error diff --git a/src/checkers/ast.rs b/src/checkers/ast.rs index 1361856d73..e6daabaf81 100644 --- a/src/checkers/ast.rs +++ b/src/checkers/ast.rs @@ -2355,7 +2355,9 @@ where } } if self.settings.enabled.contains(&CheckCode::PT009) { - if let Some(check) = flake8_pytest_style::plugins::unittest_assertion(func) { + if let Some(check) = flake8_pytest_style::plugins::unittest_assertion( + self, expr, func, args, keywords, + ) { self.checks.push(check); } } diff --git a/src/flake8_pytest_style/plugins/assertion.rs b/src/flake8_pytest_style/plugins/assertion.rs index 18bd0296a0..65ea8a3199 100644 --- a/src/flake8_pytest_style/plugins/assertion.rs +++ b/src/flake8_pytest_style/plugins/assertion.rs @@ -1,11 +1,15 @@ use rustpython_ast::{ - Boolop, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Stmt, StmtKind, Unaryop, + Boolop, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Keyword, Stmt, StmtKind, Unaryop, }; use super::helpers::is_falsy_constant; +use super::unittest_assert::UnittestAssert; +use crate::ast::helpers::unparse_stmt; use crate::ast::types::Range; use crate::ast::visitor; use crate::ast::visitor::Visitor; +use crate::autofix::Fix; +use crate::checkers::ast::Checker; use crate::registry::{Check, CheckKind}; /// Visitor that tracks assert statements and checks if they reference @@ -58,42 +62,6 @@ where } } -const UNITTEST_ASSERT_NAMES: &[&str] = &[ - "assertAlmostEqual", - "assertAlmostEquals", - "assertDictEqual", - "assertEqual", - "assertEquals", - "assertFalse", - "assertGreater", - "assertGreaterEqual", - "assertIn", - "assertIs", - "assertIsInstance", - "assertIsNone", - "assertIsNot", - "assertIsNotNone", - "assertItemsEqual", - "assertLess", - "assertLessEqual", - "assertMultiLineEqual", - "assertNotAlmostEqual", - "assertNotAlmostEquals", - "assertNotContains", - "assertNotEqual", - "assertNotEquals", - "assertNotIn", - "assertNotIsInstance", - "assertNotRegexpMatches", - "assertRaises", - "assertRaisesMessage", - "assertRaisesRegexp", - "assertRegexpMatches", - "assertSetEqual", - "assertTrue", - "assert_", -]; - /// Check if the test expression is a composite condition. /// For example, `a and b` or `not (a or b)`. The latter is equivalent /// to `not a and not b` by De Morgan's laws. @@ -120,14 +88,30 @@ fn check_assert_in_except(name: &str, body: &[Stmt]) -> Vec { } /// PT009 -pub fn unittest_assertion(call: &Expr) -> Option { - match &call.node { +pub fn unittest_assertion( + checker: &Checker, + call: &Expr, + func: &Expr, + args: &[Expr], + keywords: &[Keyword], +) -> Option { + match &func.node { ExprKind::Attribute { attr, .. } => { - if UNITTEST_ASSERT_NAMES.contains(&attr.as_str()) { - Some(Check::new( - CheckKind::UnittestAssertion(attr.to_string()), - Range::from_located(call), - )) + if let Ok(unittest_assert) = UnittestAssert::try_from(attr.as_str()) { + let mut check = Check::new( + CheckKind::UnittestAssertion(unittest_assert.to_string()), + Range::from_located(func), + ); + if checker.patch(check.kind.code()) { + if let Ok(stmt) = unittest_assert.generate_assert(args, keywords) { + check.amend(Fix::replacement( + unparse_stmt(&stmt, checker.style), + call.location, + call.end_location.unwrap(), + )); + } + } + Some(check) } else { None } diff --git a/src/flake8_pytest_style/plugins/mod.rs b/src/flake8_pytest_style/plugins/mod.rs index 204d29e060..758e54e237 100644 --- a/src/flake8_pytest_style/plugins/mod.rs +++ b/src/flake8_pytest_style/plugins/mod.rs @@ -18,3 +18,4 @@ mod marks; mod parametrize; mod patch; mod raises; +mod unittest_assert; diff --git a/src/flake8_pytest_style/plugins/unittest_assert.rs b/src/flake8_pytest_style/plugins/unittest_assert.rs new file mode 100644 index 0000000000..690728f44a --- /dev/null +++ b/src/flake8_pytest_style/plugins/unittest_assert.rs @@ -0,0 +1,394 @@ +use std::hash::BuildHasherDefault; + +use anyhow::{anyhow, bail, Result}; +use rustc_hash::FxHashMap; +use rustpython_ast::{ + Cmpop, Constant, Expr, ExprContext, ExprKind, Keyword, Stmt, StmtKind, Unaryop, +}; + +use crate::ast::helpers::{create_expr, create_stmt}; + +pub enum UnittestAssert { + AlmostEqual, + AlmostEquals, + DictEqual, + Equal, + Equals, + False, + Greater, + GreaterEqual, + In, + Is, + IsInstance, + IsNone, + IsNot, + IsNotNone, + ItemsEqual, + Less, + LessEqual, + MultiLineEqual, + NotAlmostEqual, + NotAlmostEquals, + NotContains, + NotEqual, + NotEquals, + NotIn, + NotIsInstance, + NotRegex, + NotRegexpMatches, + Raises, + RaisesMessage, + RaisesRegexp, + Regex, + RegexpMatches, + SetEqual, + True, + Underscore, +} + +impl std::fmt::Display for UnittestAssert { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UnittestAssert::AlmostEqual => write!(f, "assertAlmostEqual"), + UnittestAssert::AlmostEquals => write!(f, "assertAlmostEquals"), + UnittestAssert::DictEqual => write!(f, "assertDictEqual"), + UnittestAssert::Equal => write!(f, "assertEqual"), + UnittestAssert::Equals => write!(f, "assertEquals"), + UnittestAssert::False => write!(f, "assertFalse"), + UnittestAssert::Greater => write!(f, "assertGreater"), + UnittestAssert::GreaterEqual => write!(f, "assertGreaterEqual"), + UnittestAssert::In => write!(f, "assertIn"), + UnittestAssert::Is => write!(f, "assertIs"), + UnittestAssert::IsInstance => write!(f, "assertIsInstance"), + UnittestAssert::IsNone => write!(f, "assertIsNone"), + UnittestAssert::IsNot => write!(f, "assertIsNot"), + UnittestAssert::IsNotNone => write!(f, "assertIsNotNone"), + UnittestAssert::ItemsEqual => write!(f, "assertItemsEqual"), + UnittestAssert::Less => write!(f, "assertLess"), + UnittestAssert::LessEqual => write!(f, "assertLessEqual"), + UnittestAssert::MultiLineEqual => write!(f, "assertMultiLineEqual"), + UnittestAssert::NotAlmostEqual => write!(f, "assertNotAlmostEqual"), + UnittestAssert::NotAlmostEquals => write!(f, "assertNotAlmostEquals"), + UnittestAssert::NotContains => write!(f, "assertNotContains"), + UnittestAssert::NotEqual => write!(f, "assertNotEqual"), + UnittestAssert::NotEquals => write!(f, "assertNotEquals"), + UnittestAssert::NotIn => write!(f, "assertNotIn"), + UnittestAssert::NotIsInstance => write!(f, "assertNotIsInstance"), + UnittestAssert::NotRegex => write!(f, "assertNotRegex"), + UnittestAssert::NotRegexpMatches => write!(f, "assertNotRegexpMatches"), + UnittestAssert::Raises => write!(f, "assertRaises"), + UnittestAssert::RaisesMessage => write!(f, "assertRaisesMessage"), + UnittestAssert::RaisesRegexp => write!(f, "assertRaisesRegexp"), + UnittestAssert::Regex => write!(f, "assertRegex"), + UnittestAssert::RegexpMatches => write!(f, "assertRegexpMatches"), + UnittestAssert::SetEqual => write!(f, "assertSetEqual"), + UnittestAssert::True => write!(f, "assertTrue"), + UnittestAssert::Underscore => write!(f, "assert_"), + } + } +} + +impl TryFrom<&str> for UnittestAssert { + type Error = String; + + fn try_from(value: &str) -> Result { + match value { + "assertAlmostEqual" => Ok(UnittestAssert::AlmostEqual), + "assertAlmostEquals" => Ok(UnittestAssert::AlmostEquals), + "assertDictEqual" => Ok(UnittestAssert::DictEqual), + "assertEqual" => Ok(UnittestAssert::Equal), + "assertEquals" => Ok(UnittestAssert::Equals), + "assertFalse" => Ok(UnittestAssert::False), + "assertGreater" => Ok(UnittestAssert::Greater), + "assertGreaterEqual" => Ok(UnittestAssert::GreaterEqual), + "assertIn" => Ok(UnittestAssert::In), + "assertIs" => Ok(UnittestAssert::Is), + "assertIsInstance" => Ok(UnittestAssert::IsInstance), + "assertIsNone" => Ok(UnittestAssert::IsNone), + "assertIsNot" => Ok(UnittestAssert::IsNot), + "assertIsNotNone" => Ok(UnittestAssert::IsNotNone), + "assertItemsEqual" => Ok(UnittestAssert::ItemsEqual), + "assertLess" => Ok(UnittestAssert::Less), + "assertLessEqual" => Ok(UnittestAssert::LessEqual), + "assertMultiLineEqual" => Ok(UnittestAssert::MultiLineEqual), + "assertNotAlmostEqual" => Ok(UnittestAssert::NotAlmostEqual), + "assertNotAlmostEquals" => Ok(UnittestAssert::NotAlmostEquals), + "assertNotContains" => Ok(UnittestAssert::NotContains), + "assertNotEqual" => Ok(UnittestAssert::NotEqual), + "assertNotEquals" => Ok(UnittestAssert::NotEquals), + "assertNotIn" => Ok(UnittestAssert::NotIn), + "assertNotIsInstance" => Ok(UnittestAssert::NotIsInstance), + "assertNotRegex" => Ok(UnittestAssert::NotRegex), + "assertNotRegexpMatches" => Ok(UnittestAssert::NotRegexpMatches), + "assertRaises" => Ok(UnittestAssert::Raises), + "assertRaisesMessage" => Ok(UnittestAssert::RaisesMessage), + "assertRaisesRegexp" => Ok(UnittestAssert::RaisesRegexp), + "assertRegex" => Ok(UnittestAssert::Regex), + "assertRegexpMatches" => Ok(UnittestAssert::RegexpMatches), + "assertSetEqual" => Ok(UnittestAssert::SetEqual), + "assertTrue" => Ok(UnittestAssert::True), + "assert_" => Ok(UnittestAssert::Underscore), + _ => Err(format!("Unknown unittest assert method: {value}")), + } + } +} + +fn assert(expr: &Expr, msg: Option<&Expr>) -> Stmt { + create_stmt(StmtKind::Assert { + test: Box::new(expr.clone()), + msg: msg.map(|msg| Box::new(msg.clone())), + }) +} + +fn compare(left: &Expr, cmpop: Cmpop, right: &Expr) -> Expr { + create_expr(ExprKind::Compare { + left: Box::new(left.clone()), + ops: vec![cmpop], + comparators: vec![right.clone()], + }) +} + +pub struct Arguments<'a> { + positional: Vec<&'a str>, + keyword: Vec<&'a str>, +} + +impl<'a> Arguments<'a> { + pub fn new(positional: Vec<&'a str>, keyword: Vec<&'a str>) -> Self { + Self { + positional, + keyword, + } + } + + pub fn contains(&self, arg: &str) -> bool { + self.positional.contains(&arg) || self.keyword.contains(&arg) + } +} + +impl UnittestAssert { + pub fn arguments(&self) -> Arguments { + match self { + UnittestAssert::AlmostEqual => { + Arguments::new(vec!["first", "second"], vec!["places", "msg", "delta"]) + } + UnittestAssert::AlmostEquals => { + Arguments::new(vec!["first", "second"], vec!["places", "msg", "delta"]) + } + UnittestAssert::DictEqual => Arguments::new(vec!["d1", "d2"], vec!["msg"]), + UnittestAssert::Equal => Arguments::new(vec!["first", "second"], vec!["msg"]), + UnittestAssert::Equals => Arguments::new(vec!["first", "second"], vec!["msg"]), + UnittestAssert::False => Arguments::new(vec!["expr"], vec!["msg"]), + UnittestAssert::Greater => Arguments::new(vec!["first", "second"], vec!["msg"]), + UnittestAssert::GreaterEqual => Arguments::new(vec!["first", "second"], vec!["msg"]), + UnittestAssert::In => Arguments::new(vec!["member", "container"], vec!["msg"]), + UnittestAssert::Is => Arguments::new(vec!["expr1", "expr2"], vec!["msg"]), + UnittestAssert::IsInstance => Arguments::new(vec!["obj", "cls"], vec!["msg"]), + UnittestAssert::IsNone => Arguments::new(vec!["expr"], vec!["msg"]), + UnittestAssert::IsNot => Arguments::new(vec!["expr1", "expr2"], vec!["msg"]), + UnittestAssert::IsNotNone => Arguments::new(vec!["expr"], vec!["msg"]), + UnittestAssert::ItemsEqual => Arguments::new(vec!["first", "second"], vec!["msg"]), + UnittestAssert::Less => Arguments::new(vec!["first", "second"], vec!["msg"]), + UnittestAssert::LessEqual => Arguments::new(vec!["first", "second"], vec!["msg"]), + UnittestAssert::MultiLineEqual => Arguments::new(vec!["first", "second"], vec!["msg"]), + UnittestAssert::NotAlmostEqual => Arguments::new(vec!["first", "second"], vec!["msg"]), + UnittestAssert::NotAlmostEquals => Arguments::new(vec!["first", "second"], vec!["msg"]), + UnittestAssert::NotContains => Arguments::new(vec!["container", "member"], vec!["msg"]), + UnittestAssert::NotEqual => Arguments::new(vec!["first", "second"], vec!["msg"]), + UnittestAssert::NotEquals => Arguments::new(vec!["first", "second"], vec!["msg"]), + UnittestAssert::NotIn => Arguments::new(vec!["member", "container"], vec!["msg"]), + UnittestAssert::NotIsInstance => Arguments::new(vec!["obj", "cls"], vec!["msg"]), + UnittestAssert::NotRegex => Arguments::new(vec!["text", "regex"], vec!["msg"]), + UnittestAssert::NotRegexpMatches => Arguments::new(vec!["text", "regex"], vec!["msg"]), + UnittestAssert::Raises => Arguments::new(vec!["exception"], vec!["msg"]), + UnittestAssert::RaisesMessage => Arguments::new(vec!["exception", "msg"], vec!["msg"]), + UnittestAssert::RaisesRegexp => Arguments::new(vec!["exception", "regex"], vec!["msg"]), + UnittestAssert::Regex => Arguments::new(vec!["text", "regex"], vec!["msg"]), + UnittestAssert::RegexpMatches => Arguments::new(vec!["text", "regex"], vec!["msg"]), + UnittestAssert::SetEqual => Arguments::new(vec!["set1", "set2"], vec!["msg"]), + UnittestAssert::True => Arguments::new(vec!["expr"], vec!["msg"]), + UnittestAssert::Underscore => Arguments::new(vec!["expr"], vec!["msg"]), + } + } + + /// Create a map from argument name to value. + pub fn args_map<'a>( + &'a self, + args: &'a [Expr], + keywords: &'a [Keyword], + ) -> Result> { + if args + .iter() + .any(|arg| matches!(arg.node, ExprKind::Starred { .. })) + || keywords.iter().any(|kw| kw.node.arg.is_none()) + { + bail!("Contains variable-length arguments. Cannot autofix.".to_string()); + } + + let mut args_map: FxHashMap<&str, &Expr> = FxHashMap::with_capacity_and_hasher( + args.len() + keywords.len(), + BuildHasherDefault::default(), + ); + let arguments = self.arguments(); + for (arg, value) in arguments.positional.iter().zip(args.iter()) { + args_map.insert(arg, value); + } + for kw in keywords { + let arg = kw.node.arg.as_ref().unwrap(); + if !arguments.contains((*arg).as_str()) { + bail!("Unexpected keyword argument `{arg}`"); + } + args_map.insert(kw.node.arg.as_ref().unwrap().as_str(), &kw.node.value); + } + Ok(args_map) + } + + pub fn generate_assert(&self, args: &[Expr], keywords: &[Keyword]) -> Result { + let args = self.args_map(args, keywords)?; + match self { + UnittestAssert::True | UnittestAssert::False => { + let expr = args.get("expr").ok_or(anyhow!("Missing argument `expr`"))?; + let msg = args.get("msg").copied(); + let bool = create_expr(ExprKind::Constant { + value: Constant::Bool(matches!(self, UnittestAssert::True)), + kind: None, + }); + let expr = compare(expr, Cmpop::Is, &bool); + Ok(assert(&expr, msg)) + } + UnittestAssert::Equal + | UnittestAssert::Equals + | UnittestAssert::NotEqual + | UnittestAssert::NotEquals + | UnittestAssert::Greater + | UnittestAssert::GreaterEqual + | UnittestAssert::Less + | UnittestAssert::LessEqual => { + let first = args + .get("first") + .ok_or(anyhow!("Missing argument `first`"))?; + let second = args + .get("second") + .ok_or(anyhow!("Missing argument `second`"))?; + let msg = args.get("msg").copied(); + let cmpop = match self { + UnittestAssert::Equal | UnittestAssert::Equals => Cmpop::Eq, + UnittestAssert::NotEqual | UnittestAssert::NotEquals => Cmpop::NotEq, + UnittestAssert::Greater => Cmpop::Gt, + UnittestAssert::GreaterEqual => Cmpop::GtE, + UnittestAssert::Less => Cmpop::Lt, + UnittestAssert::LessEqual => Cmpop::LtE, + _ => unreachable!(), + }; + let expr = compare(first, cmpop, second); + Ok(assert(&expr, msg)) + } + UnittestAssert::Is | UnittestAssert::IsNot => { + let expr1 = args + .get("expr1") + .ok_or(anyhow!("Missing argument `expr1`"))?; + let expr2 = args + .get("expr2") + .ok_or(anyhow!("Missing argument `expr2`"))?; + let msg = args.get("msg").copied(); + let cmpop = if matches!(self, UnittestAssert::Is) { + Cmpop::Is + } else { + Cmpop::IsNot + }; + let expr = compare(expr1, cmpop, expr2); + Ok(assert(&expr, msg)) + } + UnittestAssert::In | UnittestAssert::NotIn => { + let member = args + .get("member") + .ok_or(anyhow!("Missing argument `member`"))?; + let container = args + .get("container") + .ok_or(anyhow!("Missing argument `container`"))?; + let msg = args.get("msg").copied(); + let cmpop = if matches!(self, UnittestAssert::In) { + Cmpop::In + } else { + Cmpop::NotIn + }; + let expr = compare(member, cmpop, container); + Ok(assert(&expr, msg)) + } + UnittestAssert::IsNone | UnittestAssert::IsNotNone => { + let expr = args.get("expr").ok_or(anyhow!("Missing argument `expr`"))?; + let msg = args.get("msg").copied(); + let cmpop = if matches!(self, UnittestAssert::IsNone) { + Cmpop::Is + } else { + Cmpop::IsNot + }; + let expr = compare( + expr, + cmpop, + &create_expr(ExprKind::Constant { + value: Constant::None, + kind: None, + }), + ); + Ok(assert(&expr, msg)) + } + UnittestAssert::IsInstance | UnittestAssert::NotIsInstance => { + let obj = args.get("obj").ok_or(anyhow!("Missing argument `obj`"))?; + let cls = args.get("cls").ok_or(anyhow!("Missing argument `cls`"))?; + let msg = args.get("msg").copied(); + let isinstance = create_expr(ExprKind::Call { + func: Box::new(create_expr(ExprKind::Name { + id: "isinstance".to_string(), + ctx: ExprContext::Load, + })), + args: vec![(**obj).clone(), (**cls).clone()], + keywords: vec![], + }); + if matches!(self, UnittestAssert::IsInstance) { + Ok(assert(&isinstance, msg)) + } else { + let expr = create_expr(ExprKind::UnaryOp { + op: Unaryop::Not, + operand: Box::new(isinstance), + }); + Ok(assert(&expr, msg)) + } + } + UnittestAssert::Regex + | UnittestAssert::RegexpMatches + | UnittestAssert::NotRegex + | UnittestAssert::NotRegexpMatches => { + let regex = args + .get("regex") + .ok_or(anyhow!("Missing argument `regex`"))?; + let text = args.get("text").ok_or(anyhow!("Missing argument `text`"))?; + let msg = args.get("msg").copied(); + let re_search = create_expr(ExprKind::Call { + func: Box::new(create_expr(ExprKind::Attribute { + value: Box::new(create_expr(ExprKind::Name { + id: "re".to_string(), + ctx: ExprContext::Load, + })), + attr: "search".to_string(), + ctx: ExprContext::Load, + })), + args: vec![(**regex).clone(), (**text).clone()], + keywords: vec![], + }); + if matches!(self, UnittestAssert::Regex | UnittestAssert::RegexpMatches) { + Ok(assert(&re_search, msg)) + } else { + Ok(assert( + &create_expr(ExprKind::UnaryOp { + op: Unaryop::Not, + operand: Box::new(re_search), + }), + msg, + )) + } + } + _ => bail!("Cannot autofix `{self}`"), + } + } +} diff --git a/src/flake8_pytest_style/snapshots/ruff__flake8_pytest_style__tests__PT009.snap b/src/flake8_pytest_style/snapshots/ruff__flake8_pytest_style__tests__PT009.snap index 6c3673efc2..5ea90e5595 100644 --- a/src/flake8_pytest_style/snapshots/ruff__flake8_pytest_style__tests__PT009.snap +++ b/src/flake8_pytest_style/snapshots/ruff__flake8_pytest_style__tests__PT009.snap @@ -3,13 +3,451 @@ source: src/flake8_pytest_style/mod.rs expression: checks --- - kind: - UnittestAssertion: assertEqual + UnittestAssertion: assertTrue location: - row: 9 - column: 4 + row: 11 + column: 8 end_location: - row: 9 - column: 20 + row: 11 + column: 23 + fix: + content: assert expr is True + location: + row: 11 + column: 8 + end_location: + row: 11 + column: 29 + parent: ~ +- kind: + UnittestAssertion: assertTrue + location: + row: 12 + column: 8 + end_location: + row: 12 + column: 23 + fix: + content: assert expr is True + location: + row: 12 + column: 8 + end_location: + row: 12 + column: 34 + parent: ~ +- kind: + UnittestAssertion: assertTrue + location: + row: 13 + column: 8 + end_location: + row: 13 + column: 23 + fix: + content: assert expr is True + location: + row: 13 + column: 8 + end_location: + row: 13 + column: 34 + parent: ~ +- kind: + UnittestAssertion: assertTrue + location: + row: 14 + column: 8 + end_location: + row: 14 + column: 23 + fix: + content: "assert expr is True, msg" + location: + row: 14 + column: 8 + end_location: + row: 14 + column: 43 + parent: ~ +- kind: + UnittestAssertion: assertTrue + location: + row: 15 + column: 8 + end_location: + row: 15 + column: 23 + fix: + content: "assert expr is True, msg" + location: + row: 15 + column: 8 + end_location: + row: 15 + column: 43 + parent: ~ +- kind: + UnittestAssertion: assertTrue + location: + row: 16 + column: 8 + end_location: + row: 16 + column: 23 fix: ~ parent: ~ +- kind: + UnittestAssertion: assertTrue + location: + row: 17 + column: 8 + end_location: + row: 17 + column: 23 + fix: ~ + parent: ~ +- kind: + UnittestAssertion: assertTrue + location: + row: 18 + column: 8 + end_location: + row: 18 + column: 23 + fix: ~ + parent: ~ +- kind: + UnittestAssertion: assertTrue + location: + row: 19 + column: 8 + end_location: + row: 19 + column: 23 + fix: ~ + parent: ~ +- kind: + UnittestAssertion: assertFalse + location: + row: 22 + column: 8 + end_location: + row: 22 + column: 24 + fix: + content: assert True is False + location: + row: 22 + column: 8 + end_location: + row: 22 + column: 30 + parent: ~ +- kind: + UnittestAssertion: assertEqual + location: + row: 25 + column: 8 + end_location: + row: 25 + column: 24 + fix: + content: assert 1 == 2 + location: + row: 25 + column: 8 + end_location: + row: 25 + column: 30 + parent: ~ +- kind: + UnittestAssertion: assertNotEqual + location: + row: 28 + column: 8 + end_location: + row: 28 + column: 27 + fix: + content: assert 1 != 1 + location: + row: 28 + column: 8 + end_location: + row: 28 + column: 33 + parent: ~ +- kind: + UnittestAssertion: assertGreater + location: + row: 31 + column: 8 + end_location: + row: 31 + column: 26 + fix: + content: assert 1 > 2 + location: + row: 31 + column: 8 + end_location: + row: 31 + column: 32 + parent: ~ +- kind: + UnittestAssertion: assertGreaterEqual + location: + row: 34 + column: 8 + end_location: + row: 34 + column: 31 + fix: + content: assert 1 >= 2 + location: + row: 34 + column: 8 + end_location: + row: 34 + column: 37 + parent: ~ +- kind: + UnittestAssertion: assertLess + location: + row: 37 + column: 8 + end_location: + row: 37 + column: 23 + fix: + content: assert 2 < 1 + location: + row: 37 + column: 8 + end_location: + row: 37 + column: 29 + parent: ~ +- kind: + UnittestAssertion: assertLessEqual + location: + row: 40 + column: 8 + end_location: + row: 40 + column: 28 + fix: + content: assert 1 <= 2 + location: + row: 40 + column: 8 + end_location: + row: 40 + column: 34 + parent: ~ +- kind: + UnittestAssertion: assertIn + location: + row: 43 + column: 8 + end_location: + row: 43 + column: 21 + fix: + content: "assert 1 in [2, 3]" + location: + row: 43 + column: 8 + end_location: + row: 43 + column: 32 + parent: ~ +- kind: + UnittestAssertion: assertNotIn + location: + row: 46 + column: 8 + end_location: + row: 46 + column: 24 + fix: + content: "assert 2 not in [2, 3]" + location: + row: 46 + column: 8 + end_location: + row: 46 + column: 35 + parent: ~ +- kind: + UnittestAssertion: assertIsNone + location: + row: 49 + column: 8 + end_location: + row: 49 + column: 25 + fix: + content: assert 0 is None + location: + row: 49 + column: 8 + end_location: + row: 49 + column: 28 + parent: ~ +- kind: + UnittestAssertion: assertIsNotNone + location: + row: 52 + column: 8 + end_location: + row: 52 + column: 28 + fix: + content: assert 0 is not None + location: + row: 52 + column: 8 + end_location: + row: 52 + column: 31 + parent: ~ +- kind: + UnittestAssertion: assertIs + location: + row: 55 + column: 8 + end_location: + row: 55 + column: 21 + fix: + content: "assert [] is []" + location: + row: 55 + column: 8 + end_location: + row: 55 + column: 29 + parent: ~ +- kind: + UnittestAssertion: assertIsNot + location: + row: 58 + column: 8 + end_location: + row: 58 + column: 24 + fix: + content: assert 1 is not 1 + location: + row: 58 + column: 8 + end_location: + row: 58 + column: 30 + parent: ~ +- kind: + UnittestAssertion: assertIsInstance + location: + row: 61 + column: 8 + end_location: + row: 61 + column: 29 + fix: + content: "assert isinstance(1, str)" + location: + row: 61 + column: 8 + end_location: + row: 61 + column: 37 + parent: ~ +- kind: + UnittestAssertion: assertNotIsInstance + location: + row: 64 + column: 8 + end_location: + row: 64 + column: 32 + fix: + content: "assert not isinstance(1, int)" + location: + row: 64 + column: 8 + end_location: + row: 64 + column: 40 + parent: ~ +- kind: + UnittestAssertion: assertRegex + location: + row: 67 + column: 8 + end_location: + row: 67 + column: 24 + fix: + content: "assert re.search(\"def\", \"abc\")" + location: + row: 67 + column: 8 + end_location: + row: 67 + column: 39 + parent: ~ +- kind: + UnittestAssertion: assertNotRegex + location: + row: 70 + column: 8 + end_location: + row: 70 + column: 27 + fix: + content: "assert not re.search(\"abc\", \"abc\")" + location: + row: 70 + column: 8 + end_location: + row: 70 + column: 42 + parent: ~ +- kind: + UnittestAssertion: assertRegexpMatches + location: + row: 73 + column: 8 + end_location: + row: 73 + column: 32 + fix: + content: "assert re.search(\"def\", \"abc\")" + location: + row: 73 + column: 8 + end_location: + row: 73 + column: 47 + parent: ~ +- kind: + UnittestAssertion: assertNotRegex + location: + row: 76 + column: 8 + end_location: + row: 76 + column: 27 + fix: + content: "assert not re.search(\"abc\", \"abc\")" + location: + row: 76 + column: 8 + end_location: + row: 76 + column: 42 + parent: ~ diff --git a/src/registry.rs b/src/registry.rs index 13480fad38..a8d8af1355 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -3799,6 +3799,7 @@ impl CheckKind { | CheckKind::TrueFalseComparison(..) | CheckKind::TypeOfPrimitive(..) | CheckKind::TypingTextStrAlias + | CheckKind::UnittestAssertion(..) | CheckKind::UnnecessaryBuiltinImport(..) | CheckKind::UnnecessaryCallAroundSorted(..) | CheckKind::UnnecessaryCollectionCall(..) @@ -4045,6 +4046,9 @@ impl CheckKind { CheckKind::UnnecessaryBuiltinImport(..) => { Some("Remove unnecessary builtin import".to_string()) } + CheckKind::UnittestAssertion(assertion) => { + Some(format!("Replace `{assertion}(...)` with `assert ...`")) + } CheckKind::UnnecessaryCallAroundSorted(func) => { Some(format!("Remove unnecessary `{func}` call")) }