[ruff] Check for non-context-manager use of pytest.raises, pytest.warns, and pytest.deprecated_call (RUF061) (#17368)

<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

This PR aims to close #16605.

## Summary

This PR introduces a new rule (`RUF061`) that detects non-contextmanager
usage of `pytest.raises`, `pytest.warns`, and `pytest.deprecated_call`.
This pattern is discouraged and [was proposed in
flake8-pytest-style](https://github.com/m-burst/flake8-pytest-style/pull/332),
but the corresponding PR has been open for over a month without
activity.

Additionally, this PR provides an unsafe fix for simple cases where the
non-contextmanager form can be transformed into the context manager
form. Examples of supported patterns are listed in `RUF061_raises.py`,
`RUF061_warns.py`, and `RUF061_deprecated_call.py` test files.

The more complex case from the original issue (involving two separate
statements):
```python
excinfo = pytest.raises(ValueError, int, "hello")
assert excinfo.match("^invalid literal")
```
is getting fixed like this:
```python
with pytest.raises(ValueError) as excinfo:
    int("hello")
assert excinfo.match("^invalid literal")
```
Putting match in the raises call requires multi-statement
transformation, which I am not sure how to implement.

## Test Plan

<!-- How was it tested? -->

New test files were added to cover various usages of the
non-contextmanager form of pytest.raises, warns, and deprecated_call.
This commit is contained in:
Denys Kyslytsyn 2025-06-17 02:03:54 +09:00 committed by GitHub
parent c5b58187da
commit c3aa965546
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 635 additions and 0 deletions

View file

@ -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))

View file

@ -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)

View file

@ -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))

View file

@ -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);
}

View file

@ -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),

View file

@ -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"))]

View file

@ -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<String> {
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<Self> {
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<StmtWith> {
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<StmtWith> {
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],
})
}

View file

@ -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;

View file

@ -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))()

View file

@ -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)()

View file

@ -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))()

1
ruff.schema.json generated
View file

@ -4039,6 +4039,7 @@
"RUF059",
"RUF06",
"RUF060",
"RUF061",
"RUF1",
"RUF10",
"RUF100",