diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF061_deprecated_call.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF061_deprecated_call.py new file mode 100644 index 0000000000000..63b08e487fbe5 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF061_deprecated_call.py @@ -0,0 +1,25 @@ +import warnings +import pytest + + +def raise_deprecation_warning(s): + warnings.warn(s, DeprecationWarning) + return s + + +def test_ok(): + with pytest.deprecated_call(): + raise_deprecation_warning("") + + +def test_error_trivial(): + pytest.deprecated_call(raise_deprecation_warning, "deprecated") + + +def test_error_assign(): + s = pytest.deprecated_call(raise_deprecation_warning, "deprecated") + print(s) + + +def test_error_lambda(): + pytest.deprecated_call(lambda: warnings.warn("", DeprecationWarning)) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF061_raises.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF061_raises.py new file mode 100644 index 0000000000000..d4e3d900cd59f --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF061_raises.py @@ -0,0 +1,40 @@ +import pytest + + +def func(a, b): + return a / b + + +def test_ok(): + with pytest.raises(ValueError): + raise ValueError + + +def test_ok_as(): + with pytest.raises(ValueError) as excinfo: + raise ValueError + + +def test_error_trivial(): + pytest.raises(ZeroDivisionError, func, 1, b=0) + + +def test_error_match(): + pytest.raises(ZeroDivisionError, func, 1, b=0).match("division by zero") + + +def test_error_assign(): + excinfo = pytest.raises(ZeroDivisionError, func, 1, b=0) + + +def test_error_kwargs(): + pytest.raises(func=func, expected_exception=ZeroDivisionError) + + +def test_error_multi_statement(): + excinfo = pytest.raises(ValueError, int, "hello") + assert excinfo.match("^invalid literal") + + +def test_error_lambda(): + pytest.raises(ZeroDivisionError, lambda: 1 / 0) diff --git a/crates/ruff_linter/resources/test/fixtures/ruff/RUF061_warns.py b/crates/ruff_linter/resources/test/fixtures/ruff/RUF061_warns.py new file mode 100644 index 0000000000000..d94573a090574 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/ruff/RUF061_warns.py @@ -0,0 +1,25 @@ +import warnings +import pytest + + +def raise_user_warning(s): + warnings.warn(s, UserWarning) + return s + + +def test_ok(): + with pytest.warns(UserWarning): + raise_user_warning("") + + +def test_error_trivial(): + pytest.warns(UserWarning, raise_user_warning, "warning") + + +def test_error_assign(): + s = pytest.warns(UserWarning, raise_user_warning, "warning") + print(s) + + +def test_error_lambda(): + pytest.warns(UserWarning, lambda: warnings.warn("", UserWarning)) diff --git a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs index f695c198ab482..377a50cc396a4 100644 --- a/crates/ruff_linter/src/checkers/ast/analyze/expression.rs +++ b/crates/ruff_linter/src/checkers/ast/analyze/expression.rs @@ -975,6 +975,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) { ]) { flake8_pytest_style::rules::raises_call(checker, call); } + if checker.enabled(Rule::LegacyFormPytestRaises) { + ruff::rules::legacy_raises_warns_deprecated_call(checker, call); + } if checker.any_enabled(&[Rule::PytestWarnsWithoutWarning, Rule::PytestWarnsTooBroad]) { flake8_pytest_style::rules::warns_call(checker, call); } diff --git a/crates/ruff_linter/src/codes.rs b/crates/ruff_linter/src/codes.rs index c31e989a46c1b..5ae727ce32d00 100644 --- a/crates/ruff_linter/src/codes.rs +++ b/crates/ruff_linter/src/codes.rs @@ -1027,6 +1027,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Ruff, "058") => (RuleGroup::Preview, rules::ruff::rules::StarmapZip), (Ruff, "059") => (RuleGroup::Preview, rules::ruff::rules::UnusedUnpackedVariable), (Ruff, "060") => (RuleGroup::Preview, rules::ruff::rules::InEmptyCollection), + (Ruff, "061") => (RuleGroup::Preview, rules::ruff::rules::LegacyFormPytestRaises), (Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA), (Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA), (Ruff, "102") => (RuleGroup::Preview, rules::ruff::rules::InvalidRuleCode), diff --git a/crates/ruff_linter/src/rules/ruff/mod.rs b/crates/ruff_linter/src/rules/ruff/mod.rs index 39cab6081954a..a9aeb31ae2688 100644 --- a/crates/ruff_linter/src/rules/ruff/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/mod.rs @@ -100,6 +100,9 @@ mod tests { #[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_2.py"))] #[test_case(Rule::UnusedUnpackedVariable, Path::new("RUF059_3.py"))] #[test_case(Rule::InEmptyCollection, Path::new("RUF060.py"))] + #[test_case(Rule::LegacyFormPytestRaises, Path::new("RUF061_raises.py"))] + #[test_case(Rule::LegacyFormPytestRaises, Path::new("RUF061_warns.py"))] + #[test_case(Rule::LegacyFormPytestRaises, Path::new("RUF061_deprecated_call.py"))] #[test_case(Rule::RedirectedNOQA, Path::new("RUF101_0.py"))] #[test_case(Rule::RedirectedNOQA, Path::new("RUF101_1.py"))] #[test_case(Rule::InvalidRuleCode, Path::new("RUF102.py"))] diff --git a/crates/ruff_linter/src/rules/ruff/rules/legacy_form_pytest_raises.rs b/crates/ruff_linter/src/rules/ruff/rules/legacy_form_pytest_raises.rs new file mode 100644 index 0000000000000..351d548334a10 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/rules/legacy_form_pytest_raises.rs @@ -0,0 +1,307 @@ +use itertools::{Either, Itertools}; +use ruff_diagnostics::{Edit, Fix}; +use ruff_macros::{ViolationMetadata, derive_message_formats}; +use ruff_python_ast::{self as ast, AtomicNodeIndex, Expr, Stmt, StmtExpr, StmtWith, WithItem}; +use ruff_python_semantic::SemanticModel; +use ruff_python_trivia::{has_leading_content, has_trailing_content, leading_indentation}; +use ruff_source_file::UniversalNewlines; +use ruff_text_size::{Ranged, TextRange}; +use std::fmt; + +use crate::{FixAvailability, Violation, checkers::ast::Checker}; + +/// ## What it does +/// Checks for non-contextmanager use of `pytest.raises`, `pytest.warns`, and `pytest.deprecated_call`. +/// +/// ## Why is this bad? +/// The context-manager form is more readable, easier to extend, and supports additional kwargs. +/// +/// ## Example +/// ```python +/// import pytest +/// +/// +/// excinfo = pytest.raises(ValueError, int, "hello") +/// pytest.warns(UserWarning, my_function, arg) +/// pytest.deprecated_call(my_deprecated_function, arg1, arg2) +/// ``` +/// +/// Use instead: +/// ```python +/// import pytest +/// +/// +/// with pytest.raises(ValueError) as excinfo: +/// int("hello") +/// with pytest.warns(UserWarning): +/// my_function(arg) +/// with pytest.deprecated_call(): +/// my_deprecated_function(arg1, arg2) +/// ``` +/// +/// ## References +/// - [`pytest` documentation: `pytest.raises`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-raises) +/// - [`pytest` documentation: `pytest.warns`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-warns) +/// - [`pytest` documentation: `pytest.deprecated_call`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-deprecated-call) +#[derive(ViolationMetadata)] +pub(crate) struct LegacyFormPytestRaises { + context_type: PytestContextType, +} + +impl Violation for LegacyFormPytestRaises { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + + #[derive_message_formats] + fn message(&self) -> String { + format!( + "Use context-manager form of `pytest.{}()`", + self.context_type + ) + } + + fn fix_title(&self) -> Option { + Some(format!( + "Use `pytest.{}()` as a context-manager", + self.context_type + )) + } +} + +/// Enum representing the type of pytest context manager +#[derive(PartialEq, Clone, Copy)] +enum PytestContextType { + Raises, + Warns, + DeprecatedCall, +} + +impl fmt::Display for PytestContextType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match self { + Self::Raises => "raises", + Self::Warns => "warns", + Self::DeprecatedCall => "deprecated_call", + }; + write!(f, "{name}") + } +} + +impl PytestContextType { + fn from_expr_name(func: &Expr, semantic: &SemanticModel) -> Option { + semantic + .resolve_qualified_name(func) + .and_then(|qualified_name| match qualified_name.segments() { + ["pytest", "raises"] => Some(Self::Raises), + ["pytest", "warns"] => Some(Self::Warns), + ["pytest", "deprecated_call"] => Some(Self::DeprecatedCall), + _ => None, + }) + } + + fn expected_arg(self) -> Option<(&'static str, usize)> { + match self { + Self::Raises => Some(("expected_exception", 0)), + Self::Warns => Some(("expected_warning", 0)), + Self::DeprecatedCall => None, + } + } + + fn func_arg(self) -> (&'static str, usize) { + match self { + Self::Raises | Self::Warns => ("func", 1), + Self::DeprecatedCall => ("func", 0), + } + } +} + +pub(crate) fn legacy_raises_warns_deprecated_call(checker: &Checker, call: &ast::ExprCall) { + let semantic = checker.semantic(); + let Some(context_type) = PytestContextType::from_expr_name(&call.func, semantic) else { + return; + }; + + let (func_arg_name, func_arg_position) = context_type.func_arg(); + if call + .arguments + .find_argument(func_arg_name, func_arg_position) + .is_none() + { + return; + } + + let mut diagnostic = + checker.report_diagnostic(LegacyFormPytestRaises { context_type }, call.range()); + + let stmt = semantic.current_statement(); + if !has_leading_content(stmt.start(), checker.source()) + && !has_trailing_content(stmt.end(), checker.source()) + { + if let Some(with_stmt) = try_fix_legacy_call(context_type, stmt, semantic) { + let generated = checker.generator().stmt(&Stmt::With(with_stmt)); + let first_line = checker.locator().line_str(stmt.start()); + let indentation = leading_indentation(first_line); + let mut indented = String::new(); + for (idx, line) in generated.universal_newlines().enumerate() { + if idx == 0 { + indented.push_str(&line); + } else { + indented.push_str(checker.stylist().line_ending().as_str()); + indented.push_str(indentation); + indented.push_str(&line); + } + } + + diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement( + indented, + stmt.range(), + ))); + } + } +} + +fn try_fix_legacy_call( + context_type: PytestContextType, + stmt: &Stmt, + semantic: &SemanticModel, +) -> Option { + match stmt { + Stmt::Expr(StmtExpr { value, .. }) => { + let call = value.as_call_expr()?; + + // Handle two patterns for legacy calls: + // 1. Direct usage: `pytest.raises(ZeroDivisionError, func, 1, b=0)` + // 2. With match method: `pytest.raises(ZeroDivisionError, func, 1, b=0).match("division by zero")` + // + // The second branch specifically looks for raises().match() pattern which only exists for + // `raises` (not `warns`/`deprecated_call`) since only `raises` returns an ExceptionInfo + // object with a .match() method. We need to preserve this match condition when converting + // to context manager form. + if PytestContextType::from_expr_name(&call.func, semantic) == Some(context_type) { + generate_with_statement(context_type, call, None, None, None) + } else if let PytestContextType::Raises = context_type { + let inner_raises_call = call + .func + .as_attribute_expr() + .filter(|expr_attribute| &expr_attribute.attr == "match") + .and_then(|expr_attribute| expr_attribute.value.as_call_expr()) + .filter(|inner_call| { + PytestContextType::from_expr_name(&inner_call.func, semantic) + == Some(PytestContextType::Raises) + })?; + let match_arg = call.arguments.args.first(); + generate_with_statement(context_type, inner_raises_call, match_arg, None, None) + } else { + None + } + } + Stmt::Assign(ast::StmtAssign { targets, value, .. }) => { + let call = value.as_call_expr().filter(|call| { + PytestContextType::from_expr_name(&call.func, semantic) == Some(context_type) + })?; + let (optional_vars, assign_targets) = match context_type { + PytestContextType::Raises => { + let [target] = targets.as_slice() else { + return None; + }; + (Some(target), None) + } + PytestContextType::Warns | PytestContextType::DeprecatedCall => { + (None, Some(targets.as_slice())) + } + }; + + generate_with_statement(context_type, call, None, optional_vars, assign_targets) + } + _ => None, + } +} + +fn generate_with_statement( + context_type: PytestContextType, + legacy_call: &ast::ExprCall, + match_arg: Option<&Expr>, + optional_vars: Option<&Expr>, + assign_targets: Option<&[Expr]>, +) -> Option { + let expected = if let Some((name, position)) = context_type.expected_arg() { + Some(legacy_call.arguments.find_argument_value(name, position)?) + } else { + None + }; + + let (func_arg_name, func_arg_position) = context_type.func_arg(); + let func = legacy_call + .arguments + .find_argument_value(func_arg_name, func_arg_position)?; + + let (func_args, func_keywords): (Vec<_>, Vec<_>) = legacy_call + .arguments + .arguments_source_order() + .skip(if expected.is_some() { 2 } else { 1 }) + .partition_map(|arg_or_keyword| match arg_or_keyword { + ast::ArgOrKeyword::Arg(expr) => Either::Left(expr.clone()), + ast::ArgOrKeyword::Keyword(keyword) => Either::Right(keyword.clone()), + }); + + let context_call = ast::ExprCall { + node_index: AtomicNodeIndex::dummy(), + range: TextRange::default(), + func: legacy_call.func.clone(), + arguments: ast::Arguments { + node_index: AtomicNodeIndex::dummy(), + range: TextRange::default(), + args: expected.cloned().as_slice().into(), + keywords: match_arg + .map(|expr| ast::Keyword { + node_index: AtomicNodeIndex::dummy(), + // Take range from the original expression so that the keyword + // argument is generated after positional arguments + range: expr.range(), + arg: Some(ast::Identifier::new("match", TextRange::default())), + value: expr.clone(), + }) + .as_slice() + .into(), + }, + }; + + let func_call = ast::ExprCall { + node_index: AtomicNodeIndex::dummy(), + range: TextRange::default(), + func: Box::new(func.clone()), + arguments: ast::Arguments { + node_index: AtomicNodeIndex::dummy(), + range: TextRange::default(), + args: func_args.into(), + keywords: func_keywords.into(), + }, + }; + + let body = if let Some(assign_targets) = assign_targets { + Stmt::Assign(ast::StmtAssign { + node_index: AtomicNodeIndex::dummy(), + range: TextRange::default(), + targets: assign_targets.to_vec(), + value: Box::new(func_call.into()), + }) + } else { + Stmt::Expr(StmtExpr { + node_index: AtomicNodeIndex::dummy(), + range: TextRange::default(), + value: Box::new(func_call.into()), + }) + }; + + Some(StmtWith { + node_index: AtomicNodeIndex::dummy(), + range: TextRange::default(), + is_async: false, + items: vec![WithItem { + node_index: AtomicNodeIndex::dummy(), + range: TextRange::default(), + context_expr: context_call.into(), + optional_vars: optional_vars.map(|var| Box::new(var.clone())), + }], + body: vec![body], + }) +} diff --git a/crates/ruff_linter/src/rules/ruff/rules/mod.rs b/crates/ruff_linter/src/rules/ruff/rules/mod.rs index 4706c713c6e91..a8344ad9874d5 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/mod.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/mod.rs @@ -21,6 +21,7 @@ pub(crate) use invalid_formatter_suppression_comment::*; pub(crate) use invalid_index_type::*; pub(crate) use invalid_pyproject_toml::*; pub(crate) use invalid_rule_code::*; +pub(crate) use legacy_form_pytest_raises::*; pub(crate) use map_int_version_parsing::*; pub(crate) use missing_fstring_syntax::*; pub(crate) use mutable_class_default::*; @@ -82,6 +83,7 @@ mod invalid_formatter_suppression_comment; mod invalid_index_type; mod invalid_pyproject_toml; mod invalid_rule_code; +mod legacy_form_pytest_raises; mod map_int_version_parsing; mod missing_fstring_syntax; mod mutable_class_default; diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_deprecated_call.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_deprecated_call.py.snap new file mode 100644 index 0000000000000..57f0c51fe18bb --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_deprecated_call.py.snap @@ -0,0 +1,57 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF061_deprecated_call.py:16:5: RUF061 [*] Use context-manager form of `pytest.deprecated_call()` + | +15 | def test_error_trivial(): +16 | pytest.deprecated_call(raise_deprecation_warning, "deprecated") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 + | + = help: Use `pytest.deprecated_call()` as a context-manager + +ℹ Unsafe fix +13 13 | +14 14 | +15 15 | def test_error_trivial(): +16 |- pytest.deprecated_call(raise_deprecation_warning, "deprecated") + 16 |+ with pytest.deprecated_call(): + 17 |+ raise_deprecation_warning("deprecated") +17 18 | +18 19 | +19 20 | def test_error_assign(): + +RUF061_deprecated_call.py:20:9: RUF061 [*] Use context-manager form of `pytest.deprecated_call()` + | +19 | def test_error_assign(): +20 | s = pytest.deprecated_call(raise_deprecation_warning, "deprecated") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 +21 | print(s) + | + = help: Use `pytest.deprecated_call()` as a context-manager + +ℹ Unsafe fix +17 17 | +18 18 | +19 19 | def test_error_assign(): +20 |- s = pytest.deprecated_call(raise_deprecation_warning, "deprecated") + 20 |+ with pytest.deprecated_call(): + 21 |+ s = raise_deprecation_warning("deprecated") +21 22 | print(s) +22 23 | +23 24 | + +RUF061_deprecated_call.py:25:5: RUF061 [*] Use context-manager form of `pytest.deprecated_call()` + | +24 | def test_error_lambda(): +25 | pytest.deprecated_call(lambda: warnings.warn("", DeprecationWarning)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 + | + = help: Use `pytest.deprecated_call()` as a context-manager + +ℹ Unsafe fix +22 22 | +23 23 | +24 24 | def test_error_lambda(): +25 |- pytest.deprecated_call(lambda: warnings.warn("", DeprecationWarning)) + 25 |+ with pytest.deprecated_call(): + 26 |+ (lambda: warnings.warn("", DeprecationWarning))() diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_raises.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_raises.py.snap new file mode 100644 index 0000000000000..be2efdaee9261 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_raises.py.snap @@ -0,0 +1,114 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF061_raises.py:19:5: RUF061 [*] Use context-manager form of `pytest.raises()` + | +18 | def test_error_trivial(): +19 | pytest.raises(ZeroDivisionError, func, 1, b=0) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 + | + = help: Use `pytest.raises()` as a context-manager + +ℹ Unsafe fix +16 16 | +17 17 | +18 18 | def test_error_trivial(): +19 |- pytest.raises(ZeroDivisionError, func, 1, b=0) + 19 |+ with pytest.raises(ZeroDivisionError): + 20 |+ func(1, b=0) +20 21 | +21 22 | +22 23 | def test_error_match(): + +RUF061_raises.py:23:5: RUF061 [*] Use context-manager form of `pytest.raises()` + | +22 | def test_error_match(): +23 | pytest.raises(ZeroDivisionError, func, 1, b=0).match("division by zero") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 + | + = help: Use `pytest.raises()` as a context-manager + +ℹ Unsafe fix +20 20 | +21 21 | +22 22 | def test_error_match(): +23 |- pytest.raises(ZeroDivisionError, func, 1, b=0).match("division by zero") + 23 |+ with pytest.raises(ZeroDivisionError, match="division by zero"): + 24 |+ func(1, b=0) +24 25 | +25 26 | +26 27 | def test_error_assign(): + +RUF061_raises.py:27:15: RUF061 [*] Use context-manager form of `pytest.raises()` + | +26 | def test_error_assign(): +27 | excinfo = pytest.raises(ZeroDivisionError, func, 1, b=0) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 + | + = help: Use `pytest.raises()` as a context-manager + +ℹ Unsafe fix +24 24 | +25 25 | +26 26 | def test_error_assign(): +27 |- excinfo = pytest.raises(ZeroDivisionError, func, 1, b=0) + 27 |+ with pytest.raises(ZeroDivisionError) as excinfo: + 28 |+ func(1, b=0) +28 29 | +29 30 | +30 31 | def test_error_kwargs(): + +RUF061_raises.py:31:5: RUF061 [*] Use context-manager form of `pytest.raises()` + | +30 | def test_error_kwargs(): +31 | pytest.raises(func=func, expected_exception=ZeroDivisionError) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 + | + = help: Use `pytest.raises()` as a context-manager + +ℹ Unsafe fix +28 28 | +29 29 | +30 30 | def test_error_kwargs(): +31 |- pytest.raises(func=func, expected_exception=ZeroDivisionError) + 31 |+ with pytest.raises(ZeroDivisionError): + 32 |+ func() +32 33 | +33 34 | +34 35 | def test_error_multi_statement(): + +RUF061_raises.py:35:15: RUF061 [*] Use context-manager form of `pytest.raises()` + | +34 | def test_error_multi_statement(): +35 | excinfo = pytest.raises(ValueError, int, "hello") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 +36 | assert excinfo.match("^invalid literal") + | + = help: Use `pytest.raises()` as a context-manager + +ℹ Unsafe fix +32 32 | +33 33 | +34 34 | def test_error_multi_statement(): +35 |- excinfo = pytest.raises(ValueError, int, "hello") + 35 |+ with pytest.raises(ValueError) as excinfo: + 36 |+ int("hello") +36 37 | assert excinfo.match("^invalid literal") +37 38 | +38 39 | + +RUF061_raises.py:40:5: RUF061 [*] Use context-manager form of `pytest.raises()` + | +39 | def test_error_lambda(): +40 | pytest.raises(ZeroDivisionError, lambda: 1 / 0) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 + | + = help: Use `pytest.raises()` as a context-manager + +ℹ Unsafe fix +37 37 | +38 38 | +39 39 | def test_error_lambda(): +40 |- pytest.raises(ZeroDivisionError, lambda: 1 / 0) + 40 |+ with pytest.raises(ZeroDivisionError): + 41 |+ (lambda: 1 / 0)() diff --git a/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_warns.py.snap b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_warns.py.snap new file mode 100644 index 0000000000000..e47eb6307e3e8 --- /dev/null +++ b/crates/ruff_linter/src/rules/ruff/snapshots/ruff_linter__rules__ruff__tests__RUF061_RUF061_warns.py.snap @@ -0,0 +1,57 @@ +--- +source: crates/ruff_linter/src/rules/ruff/mod.rs +--- +RUF061_warns.py:16:5: RUF061 [*] Use context-manager form of `pytest.warns()` + | +15 | def test_error_trivial(): +16 | pytest.warns(UserWarning, raise_user_warning, "warning") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 + | + = help: Use `pytest.warns()` as a context-manager + +ℹ Unsafe fix +13 13 | +14 14 | +15 15 | def test_error_trivial(): +16 |- pytest.warns(UserWarning, raise_user_warning, "warning") + 16 |+ with pytest.warns(UserWarning): + 17 |+ raise_user_warning("warning") +17 18 | +18 19 | +19 20 | def test_error_assign(): + +RUF061_warns.py:20:9: RUF061 [*] Use context-manager form of `pytest.warns()` + | +19 | def test_error_assign(): +20 | s = pytest.warns(UserWarning, raise_user_warning, "warning") + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 +21 | print(s) + | + = help: Use `pytest.warns()` as a context-manager + +ℹ Unsafe fix +17 17 | +18 18 | +19 19 | def test_error_assign(): +20 |- s = pytest.warns(UserWarning, raise_user_warning, "warning") + 20 |+ with pytest.warns(UserWarning): + 21 |+ s = raise_user_warning("warning") +21 22 | print(s) +22 23 | +23 24 | + +RUF061_warns.py:25:5: RUF061 [*] Use context-manager form of `pytest.warns()` + | +24 | def test_error_lambda(): +25 | pytest.warns(UserWarning, lambda: warnings.warn("", UserWarning)) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF061 + | + = help: Use `pytest.warns()` as a context-manager + +ℹ Unsafe fix +22 22 | +23 23 | +24 24 | def test_error_lambda(): +25 |- pytest.warns(UserWarning, lambda: warnings.warn("", UserWarning)) + 25 |+ with pytest.warns(UserWarning): + 26 |+ (lambda: warnings.warn("", UserWarning))() diff --git a/ruff.schema.json b/ruff.schema.json index 7bf97547310a7..4a186a0568349 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -4039,6 +4039,7 @@ "RUF059", "RUF06", "RUF060", + "RUF061", "RUF1", "RUF10", "RUF100",