mirror of
https://github.com/astral-sh/ruff.git
synced 2025-12-23 09:19:58 +00:00
Merge 51fd2f8b25 into 5ea30c4c53
This commit is contained in:
commit
01b77f01c5
7 changed files with 227 additions and 0 deletions
71
crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT101.py
vendored
Normal file
71
crates/ruff_linter/resources/test/fixtures/flake8_pytest_style/PT101.py
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import pytest
|
||||
|
||||
|
||||
# Error: boolean value without ids
|
||||
@pytest.mark.parametrize("flag", [True, False])
|
||||
def test_error_simple(flag):
|
||||
...
|
||||
|
||||
|
||||
# Error: boolean values in tuples without ids
|
||||
@pytest.mark.parametrize(("x", "flag"), [(1, True), (2, False)])
|
||||
def test_error_multi_param(x, flag):
|
||||
...
|
||||
|
||||
|
||||
# Error: boolean value in list without ids
|
||||
@pytest.mark.parametrize("enabled", [True])
|
||||
def test_error_single_bool(enabled):
|
||||
...
|
||||
|
||||
|
||||
# Error: multiple boolean values without ids
|
||||
@pytest.mark.parametrize("x", [True, True, False])
|
||||
def test_error_multiple_bools(x):
|
||||
...
|
||||
|
||||
|
||||
# Error: nested booleans without ids
|
||||
@pytest.mark.parametrize(
|
||||
("a", "b", "c"),
|
||||
[
|
||||
(1, True, "foo"),
|
||||
(2, False, "bar"),
|
||||
],
|
||||
)
|
||||
def test_error_nested(a, b, c):
|
||||
...
|
||||
|
||||
|
||||
# OK: has ids keyword argument
|
||||
@pytest.mark.parametrize("flag", [True, False], ids=["enabled", "disabled"])
|
||||
def test_ok_with_ids(flag):
|
||||
...
|
||||
|
||||
|
||||
# OK: no boolean values
|
||||
@pytest.mark.parametrize("x", [1, 2, 3])
|
||||
def test_ok_no_bools(x):
|
||||
...
|
||||
|
||||
|
||||
# OK: strings, not booleans
|
||||
@pytest.mark.parametrize("x", ["True", "False"])
|
||||
def test_ok_strings(x):
|
||||
...
|
||||
|
||||
|
||||
# OK: multiple params, no booleans
|
||||
@pytest.mark.parametrize(("x", "y"), [(1, 2), (3, 4)])
|
||||
def test_ok_multi_no_bools(x, y):
|
||||
...
|
||||
|
||||
|
||||
# OK: has ids even with nested booleans
|
||||
@pytest.mark.parametrize(
|
||||
("x", "flag"),
|
||||
[(1, True), (2, False)],
|
||||
ids=["case1", "case2"],
|
||||
)
|
||||
def test_ok_nested_with_ids(x, flag):
|
||||
...
|
||||
|
|
@ -996,6 +996,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
|||
Rule::PytestParametrizeNamesWrongType,
|
||||
Rule::PytestParametrizeValuesWrongType,
|
||||
Rule::PytestDuplicateParametrizeTestCases,
|
||||
Rule::PytestParametrizeBoolWithoutIds,
|
||||
]) {
|
||||
flake8_pytest_style::rules::parametrize(checker, call);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -877,6 +877,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
|||
(Flake8PytestStyle, "029") => rules::flake8_pytest_style::rules::PytestWarnsWithoutWarning,
|
||||
(Flake8PytestStyle, "030") => rules::flake8_pytest_style::rules::PytestWarnsTooBroad,
|
||||
(Flake8PytestStyle, "031") => rules::flake8_pytest_style::rules::PytestWarnsWithMultipleStatements,
|
||||
(Flake8PytestStyle, "101") => rules::flake8_pytest_style::rules::PytestParametrizeBoolWithoutIds,
|
||||
|
||||
// flake8-pie
|
||||
(Flake8Pie, "790") => rules::flake8_pie::rules::UnnecessaryPlaceholder,
|
||||
|
|
|
|||
|
|
@ -195,6 +195,12 @@ mod tests {
|
|||
Settings::default(),
|
||||
"PT015"
|
||||
)]
|
||||
#[test_case(
|
||||
Rule::PytestParametrizeBoolWithoutIds,
|
||||
Path::new("PT101.py"),
|
||||
Settings::default(),
|
||||
"PT101"
|
||||
)]
|
||||
#[test_case(
|
||||
Rule::PytestFailWithoutMessage,
|
||||
Path::new("PT016.py"),
|
||||
|
|
|
|||
|
|
@ -284,6 +284,59 @@ impl Violation for PytestDuplicateParametrizeTestCases {
|
|||
}
|
||||
}
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `pytest.mark.parametrize` calls where the parameter values contain
|
||||
/// booleans but no `ids` keyword argument is provided.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// When using boolean values in parametrized tests, the test names can be unclear
|
||||
/// without explicit IDs. For example, `test_foo[True]` and `test_foo[False]` don't
|
||||
/// convey what the boolean represents.
|
||||
///
|
||||
/// Providing explicit `ids` makes test output more readable and helps identify
|
||||
/// which test case failed.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.parametrize("flag", [True, False])
|
||||
/// def test_foo(flag): ...
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.parametrize(("x", "flag"), [(1, True), (2, False)])
|
||||
/// def test_bar(x, flag): ...
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// import pytest
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.parametrize("flag", [True, False], ids=["enabled", "disabled"])
|
||||
/// def test_foo(flag): ...
|
||||
///
|
||||
///
|
||||
/// @pytest.mark.parametrize(("x", "flag"), [(1, True), (2, False)], ids=["case1", "case2"])
|
||||
/// def test_bar(x, flag): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [`pytest` documentation: How to parametrize fixtures and test functions](https://docs.pytest.org/en/latest/how-to/parametrize.html#pytest-mark-parametrize)
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "v0.0.0")]
|
||||
pub(crate) struct PytestParametrizeBoolWithoutIds;
|
||||
|
||||
impl Violation for PytestParametrizeBoolWithoutIds {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"Boolean value in `pytest.mark.parametrize` without `ids` argument".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn elts_to_csv(elts: &[Expr], generator: Generator, flags: StringLiteralFlags) -> Option<String> {
|
||||
if !elts.iter().all(Expr::is_string_literal_expr) {
|
||||
return None;
|
||||
|
|
@ -848,6 +901,36 @@ fn handle_value_rows(
|
|||
}
|
||||
}
|
||||
|
||||
fn expr_contains_boolean(expr: &Expr) -> bool {
|
||||
match expr {
|
||||
Expr::BooleanLiteral(_) => true,
|
||||
Expr::Tuple(ast::ExprTuple { elts, .. }) | Expr::List(ast::ExprList { elts, .. }) => {
|
||||
elts.iter().any(expr_contains_boolean)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// PT101
|
||||
fn check_boolean_without_ids(checker: &Checker, call: &ExprCall) {
|
||||
let Some(values) = call.arguments.find_argument_value("argvalues", 1) else {
|
||||
// No argvalues? Nothing to check.
|
||||
return;
|
||||
};
|
||||
|
||||
if call.arguments.find_keyword("ids").is_some() {
|
||||
// Have `ids`, this is fine.
|
||||
return;
|
||||
}
|
||||
|
||||
if !expr_contains_boolean(values) {
|
||||
// No boolean values, this is fine.
|
||||
return;
|
||||
}
|
||||
|
||||
checker.report_diagnostic(PytestParametrizeBoolWithoutIds, call.range());
|
||||
}
|
||||
|
||||
pub(crate) fn parametrize(checker: &Checker, call: &ExprCall) {
|
||||
if !is_pytest_parametrize(call, checker.semantic()) {
|
||||
return;
|
||||
|
|
@ -874,4 +957,7 @@ pub(crate) fn parametrize(checker: &Checker, call: &ExprCall) {
|
|||
check_duplicates(checker, values);
|
||||
}
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::PytestParametrizeBoolWithoutIds) {
|
||||
check_boolean_without_ids(checker, call);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pytest_style/mod.rs
|
||||
---
|
||||
PT101 Boolean value in `pytest.mark.parametrize` without `ids` argument
|
||||
--> PT101.py:5:2
|
||||
|
|
||||
4 | # Error: boolean value without ids
|
||||
5 | @pytest.mark.parametrize("flag", [True, False])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
6 | def test_error_simple(flag):
|
||||
7 | ...
|
||||
|
|
||||
|
||||
PT101 Boolean value in `pytest.mark.parametrize` without `ids` argument
|
||||
--> PT101.py:11:2
|
||||
|
|
||||
10 | # Error: boolean values in tuples without ids
|
||||
11 | @pytest.mark.parametrize(("x", "flag"), [(1, True), (2, False)])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
12 | def test_error_multi_param(x, flag):
|
||||
13 | ...
|
||||
|
|
||||
|
||||
PT101 Boolean value in `pytest.mark.parametrize` without `ids` argument
|
||||
--> PT101.py:17:2
|
||||
|
|
||||
16 | # Error: boolean value in list without ids
|
||||
17 | @pytest.mark.parametrize("enabled", [True])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
18 | def test_error_single_bool(enabled):
|
||||
19 | ...
|
||||
|
|
||||
|
||||
PT101 Boolean value in `pytest.mark.parametrize` without `ids` argument
|
||||
--> PT101.py:23:2
|
||||
|
|
||||
22 | # Error: multiple boolean values without ids
|
||||
23 | @pytest.mark.parametrize("x", [True, True, False])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
24 | def test_error_multiple_bools(x):
|
||||
25 | ...
|
||||
|
|
||||
|
||||
PT101 Boolean value in `pytest.mark.parametrize` without `ids` argument
|
||||
--> PT101.py:29:2
|
||||
|
|
||||
28 | # Error: nested booleans without ids
|
||||
29 | @pytest.mark.parametrize(
|
||||
| __^
|
||||
30 | | ("a", "b", "c"),
|
||||
31 | | [
|
||||
32 | | (1, True, "foo"),
|
||||
33 | | (2, False, "bar"),
|
||||
34 | | ],
|
||||
35 | | )
|
||||
| |_^
|
||||
36 | def test_error_nested(a, b, c):
|
||||
37 | ...
|
||||
|
|
||||
3
ruff.schema.json
generated
3
ruff.schema.json
generated
|
|
@ -3850,6 +3850,9 @@
|
|||
"PT03",
|
||||
"PT030",
|
||||
"PT031",
|
||||
"PT1",
|
||||
"PT10",
|
||||
"PT101",
|
||||
"PTH",
|
||||
"PTH1",
|
||||
"PTH10",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue