[flake8-pytest-style] Implement pytest.warns diagnostics (PT029, PT030, PT031) (#15444)

## Summary

Implements upstream diagnostics `PT029`, `PT030`, `PT031` that function
as pytest.warns corollaries of `PT010`, `PT011`, `PT012` respectively.
Most of the implementation and documentation is designed to mirror those
existing diagnostics.

Closes #14239

## Test Plan

Tests for `PT029`, `PT030`, `PT031` largely copied from `PT010`,
`PT011`, `PT012` respectively.

`cargo nextest run`

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
This commit is contained in:
Tom Kuson 2025-01-13 01:46:59 +00:00 committed by GitHub
parent fa11b08766
commit 347ab5b47a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 823 additions and 0 deletions

View file

@ -0,0 +1,11 @@
import pytest
def test_ok():
with pytest.warns():
pass
def test_error():
with pytest.warns(UserWarning):
pass

View file

@ -0,0 +1,34 @@
import pytest
from foo import FooWarning
def test_ok():
with pytest.warns(UserWarning, match="Can't divide by 0"):
pass
def test_ok_different_error_from_config():
with pytest.warns(EncodingWarning):
pass
def test_error_no_argument_given():
with pytest.warns(UserWarning):
pass
with pytest.warns(expected_warning=UserWarning):
pass
with pytest.warns(FooWarning):
pass
def test_error_match_is_empty():
with pytest.warns(UserWarning, match=None):
pass
with pytest.warns(UserWarning, match=""):
pass
with pytest.warns(UserWarning, match=f""):
pass

View file

@ -0,0 +1,75 @@
import pytest
def test_ok():
with pytest.warns(UserWarning):
...
async def test_ok_trivial_with():
with pytest.warns(UserWarning):
with context_manager_under_test():
pass
with pytest.warns(UserWarning):
with context_manager_under_test():
foo()
with pytest.warns(UserWarning):
async with context_manager_under_test():
pass
def test_ok_complex_single_call():
with pytest.warns(UserWarning):
foo(
"",
0,
)
def test_ok_func_and_class():
with pytest.warns(UserWarning):
class Foo:
pass
with pytest.warns(UserWarning):
def foo():
pass
def test_error_multiple_statements():
with pytest.warns(UserWarning):
foo()
bar()
async def test_error_complex_statement():
with pytest.warns(UserWarning):
if True:
foo()
with pytest.warns(UserWarning):
for i in []:
foo()
with pytest.warns(UserWarning):
async for i in []:
foo()
with pytest.warns(UserWarning):
while True:
foo()
with pytest.warns(UserWarning):
async with context_manager_under_test():
if True:
foo()
def test_error_try():
with pytest.warns(UserWarning):
try:
foo()
except:
raise

View file

@ -915,6 +915,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
]) {
flake8_pytest_style::rules::raises_call(checker, call);
}
if checker.any_enabled(&[Rule::PytestWarnsWithoutWarning, Rule::PytestWarnsTooBroad]) {
flake8_pytest_style::rules::warns_call(checker, call);
}
if checker.enabled(Rule::PytestFailWithoutMessage) {
flake8_pytest_style::rules::fail_call(checker, call);
}

View file

@ -1296,6 +1296,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::PytestRaisesWithMultipleStatements) {
flake8_pytest_style::rules::complex_raises(checker, stmt, items, body);
}
if checker.enabled(Rule::PytestWarnsWithMultipleStatements) {
flake8_pytest_style::rules::complex_warns(checker, stmt, items, body);
}
if checker.enabled(Rule::MultipleWithStatements) {
flake8_simplify::rules::multiple_with_statements(
checker,

View file

@ -830,6 +830,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8PytestStyle, "025") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestErroneousUseFixturesOnFixture),
(Flake8PytestStyle, "026") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestUseFixturesWithoutParameters),
(Flake8PytestStyle, "027") => (RuleGroup::Stable, rules::flake8_pytest_style::rules::PytestUnittestRaisesAssertion),
(Flake8PytestStyle, "029") => (RuleGroup::Preview, rules::flake8_pytest_style::rules::PytestWarnsWithoutWarning),
(Flake8PytestStyle, "030") => (RuleGroup::Preview, rules::flake8_pytest_style::rules::PytestWarnsTooBroad),
(Flake8PytestStyle, "031") => (RuleGroup::Preview, rules::flake8_pytest_style::rules::PytestWarnsWithMultipleStatements),
// flake8-pie
(Flake8Pie, "790") => (RuleGroup::Stable, rules::flake8_pie::rules::UnnecessaryPlaceholder),

View file

@ -275,6 +275,60 @@ mod tests {
Settings::default(),
"PT027_1"
)]
#[test_case(
Rule::PytestWarnsWithoutWarning,
Path::new("PT029.py"),
Settings::default(),
"PT029"
)]
#[test_case(
Rule::PytestWarnsTooBroad,
Path::new("PT030.py"),
Settings::default(),
"PT030_default"
)]
#[test_case(
Rule::PytestWarnsTooBroad,
Path::new("PT030.py"),
Settings {
warns_extend_require_match_for: vec![IdentifierPattern::new("EncodingWarning").unwrap()],
..Settings::default()
},
"PT030_extend_broad_exceptions"
)]
#[test_case(
Rule::PytestWarnsTooBroad,
Path::new("PT030.py"),
Settings {
warns_require_match_for: vec![IdentifierPattern::new("EncodingWarning").unwrap()],
..Settings::default()
},
"PT030_replace_broad_exceptions"
)]
#[test_case(
Rule::PytestWarnsTooBroad,
Path::new("PT030.py"),
Settings {
warns_require_match_for: vec![IdentifierPattern::new("*").unwrap()],
..Settings::default()
},
"PT030_glob_all"
)]
#[test_case(
Rule::PytestWarnsTooBroad,
Path::new("PT030.py"),
Settings {
warns_require_match_for: vec![IdentifierPattern::new("foo.*").unwrap()],
..Settings::default()
},
"PT030_glob_prefix"
)]
#[test_case(
Rule::PytestWarnsWithMultipleStatements,
Path::new("PT031.py"),
Settings::default(),
"PT031"
)]
fn test_pytest_style(
rule_code: Rule,
path: &Path,

View file

@ -6,6 +6,7 @@ pub(crate) use marks::*;
pub(crate) use parametrize::*;
pub(crate) use patch::*;
pub(crate) use raises::*;
pub(crate) use warns::*;
mod assertion;
mod fail;
@ -17,3 +18,4 @@ mod parametrize;
mod patch;
mod raises;
mod unittest_assert;
mod warns;

View file

@ -0,0 +1,254 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::helpers::is_compound_statement;
use ruff_python_ast::{self as ast, Expr, Stmt, WithItem};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::registry::Rule;
use super::helpers::is_empty_or_null_string;
/// ## What it does
/// Checks for `pytest.warns` context managers with multiple statements.
///
/// ## Why is this bad?
/// When `pytest.warns` is used as a context manager and contains multiple
/// statements, it can lead to the test passing when it should instead fail.
///
/// A `pytest.warns` context manager should only contain a single
/// simple statement that triggers the expected warning.
///
/// ## Example
/// ```python
/// import pytest
///
///
/// def test_foo():
/// with pytest.warns(MyWarning):
/// setup()
/// func_to_test() # not executed if `setup()` triggers `MyWarning`
/// assert foo() # not executed
/// ```
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// def test_foo():
/// setup()
/// with pytest.warning(MyWarning):
/// func_to_test()
/// assert foo()
/// ```
///
/// ## References
/// - [`pytest` documentation: `pytest.warns`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-warns)
#[derive(ViolationMetadata)]
pub(crate) struct PytestWarnsWithMultipleStatements;
impl Violation for PytestWarnsWithMultipleStatements {
#[derive_message_formats]
fn message(&self) -> String {
"`pytest.warns()` block should contain a single simple statement".to_string()
}
}
/// ## What it does
/// Checks for `pytest.warns` calls without a `match` parameter.
///
/// ## Why is this bad?
/// `pytest.warns(Warning)` will catch any `Warning` and may catch warnings that
/// are unrelated to the code under test. To avoid this, `pytest.warns` should
/// be called with a `match` parameter. The warning names that require a `match`
/// parameter can be configured via the
/// [`lint.flake8-pytest-style.warns-require-match-for`] and
/// [`lint.flake8-pytest-style.warns-extend-require-match-for`] settings.
///
/// ## Example
/// ```python
/// import pytest
///
///
/// def test_foo():
/// with pytest.warns(RuntimeWarning):
/// ...
///
/// # empty string is also an error
/// with pytest.warns(RuntimeWarning, match=""):
/// ...
/// ```
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// def test_foo():
/// with pytest.warns(RuntimeWarning, match="expected message"):
/// ...
/// ```
///
/// ## Options
/// - `lint.flake8-pytest-style.warns-require-match-for`
/// - `lint.flake8-pytest-style.warns-extend-require-match-for`
///
/// ## References
/// - [`pytest` documentation: `pytest.warns`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-warns)
#[derive(ViolationMetadata)]
pub(crate) struct PytestWarnsTooBroad {
warning: String,
}
impl Violation for PytestWarnsTooBroad {
#[derive_message_formats]
fn message(&self) -> String {
let PytestWarnsTooBroad { warning } = self;
format!(
"`pytest.warns({warning})` is too broad, set the `match` parameter or use a more \
specific warning"
)
}
}
/// ## What it does
/// Checks for `pytest.warns` calls without an expected warning.
///
/// ## Why is this bad?
/// `pytest.warns` expects to receive an expected warning as its first
/// argument. If omitted, the `pytest.warns` call will fail at runtime.
///
/// ## Example
/// ```python
/// import pytest
///
///
/// def test_foo():
/// with pytest.warns():
/// do_something()
/// ```
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// def test_foo():
/// with pytest.warns(SomeWarning):
/// do_something()
/// ```
///
/// ## References
/// - [`pytest` documentation: `pytest.warns`](https://docs.pytest.org/en/latest/reference/reference.html#pytest-warns)
#[derive(ViolationMetadata)]
pub(crate) struct PytestWarnsWithoutWarning;
impl Violation for PytestWarnsWithoutWarning {
#[derive_message_formats]
fn message(&self) -> String {
"Set the expected warning in `pytest.warns()`".to_string()
}
}
pub(crate) fn is_pytest_warns(func: &Expr, semantic: &SemanticModel) -> bool {
semantic
.resolve_qualified_name(func)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pytest", "warns"]))
}
const fn is_non_trivial_with_body(body: &[Stmt]) -> bool {
if let [stmt] = body {
is_compound_statement(stmt)
} else {
true
}
}
/// PT029, PT030
pub(crate) fn warns_call(checker: &mut Checker, call: &ast::ExprCall) {
if is_pytest_warns(&call.func, checker.semantic()) {
if checker.enabled(Rule::PytestWarnsWithoutWarning) {
if call.arguments.is_empty() {
checker.diagnostics.push(Diagnostic::new(
PytestWarnsWithoutWarning,
call.func.range(),
));
}
}
if checker.enabled(Rule::PytestWarnsTooBroad) {
if let Some(warning) = call.arguments.find_argument_value("expected_warning", 0) {
if call
.arguments
.find_keyword("match")
.map_or(true, |k| is_empty_or_null_string(&k.value))
{
warning_needs_match(checker, warning);
}
}
}
}
}
/// PT031
pub(crate) fn complex_warns(checker: &mut Checker, stmt: &Stmt, items: &[WithItem], body: &[Stmt]) {
let warns_called = items.iter().any(|item| match &item.context_expr {
Expr::Call(ast::ExprCall { func, .. }) => is_pytest_warns(func, checker.semantic()),
_ => false,
});
// Check body for `pytest.warns` context manager
if warns_called {
let is_too_complex = if let [stmt] = body {
match stmt {
Stmt::With(ast::StmtWith { body, .. }) => is_non_trivial_with_body(body),
// Allow function and class definitions to test decorators
Stmt::ClassDef(_) | Stmt::FunctionDef(_) => false,
stmt => is_compound_statement(stmt),
}
} else {
true
};
if is_too_complex {
checker.diagnostics.push(Diagnostic::new(
PytestWarnsWithMultipleStatements,
stmt.range(),
));
}
}
}
/// PT030
fn warning_needs_match(checker: &mut Checker, warning: &Expr) {
if let Some(qualified_name) =
checker
.semantic()
.resolve_qualified_name(warning)
.and_then(|qualified_name| {
let qualified_name = qualified_name.to_string();
checker
.settings
.flake8_pytest_style
.warns_require_match_for
.iter()
.chain(
&checker
.settings
.flake8_pytest_style
.warns_extend_require_match_for,
)
.any(|pattern| pattern.matches(&qualified_name))
.then_some(qualified_name)
})
{
checker.diagnostics.push(Diagnostic::new(
PytestWarnsTooBroad {
warning: qualified_name,
},
warning.range(),
));
}
}

View file

@ -24,6 +24,12 @@ pub fn default_broad_exceptions() -> Vec<IdentifierPattern> {
.to_vec()
}
pub fn default_broad_warnings() -> Vec<IdentifierPattern> {
["Warning", "UserWarning", "DeprecationWarning"]
.map(|pattern| IdentifierPattern::new(pattern).expect("invalid default warning pattern"))
.to_vec()
}
#[derive(Debug, Clone, CacheKey)]
pub struct Settings {
pub fixture_parentheses: bool,
@ -33,6 +39,8 @@ pub struct Settings {
pub raises_require_match_for: Vec<IdentifierPattern>,
pub raises_extend_require_match_for: Vec<IdentifierPattern>,
pub mark_parentheses: bool,
pub warns_require_match_for: Vec<IdentifierPattern>,
pub warns_extend_require_match_for: Vec<IdentifierPattern>,
}
impl Default for Settings {
@ -45,6 +53,8 @@ impl Default for Settings {
raises_require_match_for: default_broad_exceptions(),
raises_extend_require_match_for: vec![],
mark_parentheses: false,
warns_require_match_for: default_broad_warnings(),
warns_extend_require_match_for: vec![],
}
}
}
@ -73,6 +83,8 @@ impl fmt::Display for Settings {
pub enum SettingsError {
InvalidRaisesRequireMatchFor(glob::PatternError),
InvalidRaisesExtendRequireMatchFor(glob::PatternError),
InvalidWarnsRequireMatchFor(glob::PatternError),
InvalidWarnsExtendRequireMatchFor(glob::PatternError),
}
impl fmt::Display for SettingsError {
@ -84,6 +96,12 @@ impl fmt::Display for SettingsError {
SettingsError::InvalidRaisesExtendRequireMatchFor(err) => {
write!(f, "invalid raises-extend-require-match-for pattern: {err}")
}
SettingsError::InvalidWarnsRequireMatchFor(err) => {
write!(f, "invalid warns-require-match-for pattern: {err}")
}
SettingsError::InvalidWarnsExtendRequireMatchFor(err) => {
write!(f, "invalid warns-extend-require-match-for pattern: {err}")
}
}
}
}
@ -93,6 +111,8 @@ impl Error for SettingsError {
match self {
SettingsError::InvalidRaisesRequireMatchFor(err) => Some(err),
SettingsError::InvalidRaisesExtendRequireMatchFor(err) => Some(err),
SettingsError::InvalidWarnsRequireMatchFor(err) => Some(err),
SettingsError::InvalidWarnsExtendRequireMatchFor(err) => Some(err),
}
}
}

View file

@ -0,0 +1,10 @@
---
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
---
PT029.py:5:10: PT029 Set the expected warning in `pytest.warns()`
|
4 | def test_ok():
5 | with pytest.warns():
| ^^^^^^^^^^^^ PT029
6 | pass
|

View file

@ -0,0 +1,46 @@
---
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
snapshot_kind: text
---
PT030.py:16:23: PT030 `pytest.warns(UserWarning)` is too broad, set the `match` parameter or use a more specific warning
|
15 | def test_error_no_argument_given():
16 | with pytest.warns(UserWarning):
| ^^^^^^^^^^^ PT030
17 | pass
|
PT030.py:19:40: PT030 `pytest.warns(UserWarning)` is too broad, set the `match` parameter or use a more specific warning
|
17 | pass
18 |
19 | with pytest.warns(expected_warning=UserWarning):
| ^^^^^^^^^^^ PT030
20 | pass
|
PT030.py:27:23: PT030 `pytest.warns(UserWarning)` is too broad, set the `match` parameter or use a more specific warning
|
26 | def test_error_match_is_empty():
27 | with pytest.warns(UserWarning, match=None):
| ^^^^^^^^^^^ PT030
28 | pass
|
PT030.py:30:23: PT030 `pytest.warns(UserWarning)` is too broad, set the `match` parameter or use a more specific warning
|
28 | pass
29 |
30 | with pytest.warns(UserWarning, match=""):
| ^^^^^^^^^^^ PT030
31 | pass
|
PT030.py:33:23: PT030 `pytest.warns(UserWarning)` is too broad, set the `match` parameter or use a more specific warning
|
31 | pass
32 |
33 | with pytest.warns(UserWarning, match=f""):
| ^^^^^^^^^^^ PT030
34 | pass
|

View file

@ -0,0 +1,54 @@
---
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
snapshot_kind: text
---
PT030.py:11:23: PT030 `pytest.warns(EncodingWarning)` is too broad, set the `match` parameter or use a more specific warning
|
10 | def test_ok_different_error_from_config():
11 | with pytest.warns(EncodingWarning):
| ^^^^^^^^^^^^^^^ PT030
12 | pass
|
PT030.py:16:23: PT030 `pytest.warns(UserWarning)` is too broad, set the `match` parameter or use a more specific warning
|
15 | def test_error_no_argument_given():
16 | with pytest.warns(UserWarning):
| ^^^^^^^^^^^ PT030
17 | pass
|
PT030.py:19:40: PT030 `pytest.warns(UserWarning)` is too broad, set the `match` parameter or use a more specific warning
|
17 | pass
18 |
19 | with pytest.warns(expected_warning=UserWarning):
| ^^^^^^^^^^^ PT030
20 | pass
|
PT030.py:27:23: PT030 `pytest.warns(UserWarning)` is too broad, set the `match` parameter or use a more specific warning
|
26 | def test_error_match_is_empty():
27 | with pytest.warns(UserWarning, match=None):
| ^^^^^^^^^^^ PT030
28 | pass
|
PT030.py:30:23: PT030 `pytest.warns(UserWarning)` is too broad, set the `match` parameter or use a more specific warning
|
28 | pass
29 |
30 | with pytest.warns(UserWarning, match=""):
| ^^^^^^^^^^^ PT030
31 | pass
|
PT030.py:33:23: PT030 `pytest.warns(UserWarning)` is too broad, set the `match` parameter or use a more specific warning
|
31 | pass
32 |
33 | with pytest.warns(UserWarning, match=f""):
| ^^^^^^^^^^^ PT030
34 | pass
|

View file

@ -0,0 +1,63 @@
---
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
snapshot_kind: text
---
PT030.py:11:23: PT030 `pytest.warns(EncodingWarning)` is too broad, set the `match` parameter or use a more specific warning
|
10 | def test_ok_different_error_from_config():
11 | with pytest.warns(EncodingWarning):
| ^^^^^^^^^^^^^^^ PT030
12 | pass
|
PT030.py:16:23: PT030 `pytest.warns(UserWarning)` is too broad, set the `match` parameter or use a more specific warning
|
15 | def test_error_no_argument_given():
16 | with pytest.warns(UserWarning):
| ^^^^^^^^^^^ PT030
17 | pass
|
PT030.py:19:40: PT030 `pytest.warns(UserWarning)` is too broad, set the `match` parameter or use a more specific warning
|
17 | pass
18 |
19 | with pytest.warns(expected_warning=UserWarning):
| ^^^^^^^^^^^ PT030
20 | pass
|
PT030.py:22:23: PT030 `pytest.warns(foo.FooWarning)` is too broad, set the `match` parameter or use a more specific warning
|
20 | pass
21 |
22 | with pytest.warns(FooWarning):
| ^^^^^^^^^^ PT030
23 | pass
|
PT030.py:27:23: PT030 `pytest.warns(UserWarning)` is too broad, set the `match` parameter or use a more specific warning
|
26 | def test_error_match_is_empty():
27 | with pytest.warns(UserWarning, match=None):
| ^^^^^^^^^^^ PT030
28 | pass
|
PT030.py:30:23: PT030 `pytest.warns(UserWarning)` is too broad, set the `match` parameter or use a more specific warning
|
28 | pass
29 |
30 | with pytest.warns(UserWarning, match=""):
| ^^^^^^^^^^^ PT030
31 | pass
|
PT030.py:33:23: PT030 `pytest.warns(UserWarning)` is too broad, set the `match` parameter or use a more specific warning
|
31 | pass
32 |
33 | with pytest.warns(UserWarning, match=f""):
| ^^^^^^^^^^^ PT030
34 | pass
|

View file

@ -0,0 +1,12 @@
---
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
snapshot_kind: text
---
PT030.py:22:23: PT030 `pytest.warns(foo.FooWarning)` is too broad, set the `match` parameter or use a more specific warning
|
20 | pass
21 |
22 | with pytest.warns(FooWarning):
| ^^^^^^^^^^ PT030
23 | pass
|

View file

@ -0,0 +1,11 @@
---
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
snapshot_kind: text
---
PT030.py:11:23: PT030 `pytest.warns(EncodingWarning)` is too broad, set the `match` parameter or use a more specific warning
|
10 | def test_ok_different_error_from_config():
11 | with pytest.warns(EncodingWarning):
| ^^^^^^^^^^^^^^^ PT030
12 | pass
|

View file

@ -0,0 +1,88 @@
---
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
snapshot_kind: text
---
PT031.py:42:5: PT031 `pytest.warns()` block should contain a single simple statement
|
41 | def test_error_multiple_statements():
42 | with pytest.warns(UserWarning):
| _____^
43 | | foo()
44 | | bar()
| |_____________^ PT031
|
PT031.py:48:5: PT031 `pytest.warns()` block should contain a single simple statement
|
47 | async def test_error_complex_statement():
48 | with pytest.warns(UserWarning):
| _____^
49 | | if True:
50 | | foo()
| |_________________^ PT031
51 |
52 | with pytest.warns(UserWarning):
|
PT031.py:52:5: PT031 `pytest.warns()` block should contain a single simple statement
|
50 | foo()
51 |
52 | with pytest.warns(UserWarning):
| _____^
53 | | for i in []:
54 | | foo()
| |_________________^ PT031
55 |
56 | with pytest.warns(UserWarning):
|
PT031.py:56:5: PT031 `pytest.warns()` block should contain a single simple statement
|
54 | foo()
55 |
56 | with pytest.warns(UserWarning):
| _____^
57 | | async for i in []:
58 | | foo()
| |_________________^ PT031
59 |
60 | with pytest.warns(UserWarning):
|
PT031.py:60:5: PT031 `pytest.warns()` block should contain a single simple statement
|
58 | foo()
59 |
60 | with pytest.warns(UserWarning):
| _____^
61 | | while True:
62 | | foo()
| |_________________^ PT031
63 |
64 | with pytest.warns(UserWarning):
|
PT031.py:64:5: PT031 `pytest.warns()` block should contain a single simple statement
|
62 | foo()
63 |
64 | with pytest.warns(UserWarning):
| _____^
65 | | async with context_manager_under_test():
66 | | if True:
67 | | foo()
| |_____________________^ PT031
|
PT031.py:71:5: PT031 `pytest.warns()` block should contain a single simple statement
|
70 | def test_error_try():
71 | with pytest.warns(UserWarning):
| _____^
72 | | try:
73 | | foo()
74 | | except:
75 | | raise
| |_________________^ PT031
|