mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 18:28:24 +00:00
[flake8-bugbear
] itertools.batched()
without explicit strict
(B911
) (#14408)
## Summary Resolves #14387. ## Test Plan `cargo nextest run` and `cargo insta test`. --------- Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
parent
e3f34b8f5b
commit
4b8c815b27
9 changed files with 331 additions and 3 deletions
59
crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B911.py
vendored
Normal file
59
crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B911.py
vendored
Normal file
|
@ -0,0 +1,59 @@
|
|||
from itertools import batched, count, cycle, repeat
|
||||
|
||||
|
||||
# Errors
|
||||
batched(range(3), 1)
|
||||
batched("abc", 2)
|
||||
batched([i for i in range(42)], some_n)
|
||||
batched((foo for foo in cycle()))
|
||||
batched(itertools.batched([1, 2, 3], strict=True))
|
||||
|
||||
# Errors (limited iterators).
|
||||
batched(repeat(1, 1))
|
||||
batched(repeat(1, times=4))
|
||||
|
||||
# No fix
|
||||
batched([], **kwargs)
|
||||
|
||||
# No errors
|
||||
batched()
|
||||
batched(range(3), 0, strict=True)
|
||||
batched(["a", "b"], count, strict=False)
|
||||
batched(("a", "b", "c"), zip(repeat()), strict=True)
|
||||
|
||||
# No errors (infinite iterators)
|
||||
batched(cycle("ABCDEF"), 3)
|
||||
batched(count(), qux + lorem)
|
||||
batched(repeat(1), ipsum // 19 @ 0x1)
|
||||
batched(repeat(1, None))
|
||||
batched(repeat(1, times=None))
|
||||
|
||||
|
||||
import itertools
|
||||
|
||||
# Errors
|
||||
itertools.batched(range(3), 1)
|
||||
itertools.batched("abc", 2)
|
||||
itertools.batched([i for i in range(42)], some_n)
|
||||
itertools.batched((foo for foo in cycle()))
|
||||
itertools.batched(itertools.batched([1, 2, 3], strict=True))
|
||||
|
||||
# Errors (limited iterators).
|
||||
itertools.batched(repeat(1, 1))
|
||||
itertools.batched(repeat(1, times=4))
|
||||
|
||||
# No fix
|
||||
itertools.batched([], **kwargs)
|
||||
|
||||
# No errors
|
||||
itertools.batched()
|
||||
itertools.batched(range(3), 0, strict=True)
|
||||
itertools.batched(["a", "b"], count, strict=False)
|
||||
itertools.batched(("a", "b", "c"), zip(repeat()), strict=True)
|
||||
|
||||
# No errors (infinite iterators)
|
||||
itertools.batched(cycle("ABCDEF"), 3)
|
||||
itertools.batched(count(), qux + lorem)
|
||||
itertools.batched(repeat(1), ipsum // 19 @ 0x1)
|
||||
itertools.batched(repeat(1, None))
|
||||
itertools.batched(repeat(1, times=None))
|
|
@ -1099,6 +1099,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
|||
if checker.enabled(Rule::DotlessPathlibWithSuffix) {
|
||||
flake8_use_pathlib::rules::dotless_pathlib_with_suffix(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::BatchedWithoutExplicitStrict) {
|
||||
flake8_bugbear::rules::batched_without_explicit_strict(checker, call);
|
||||
}
|
||||
}
|
||||
Expr::Dict(dict) => {
|
||||
if checker.any_enabled(&[
|
||||
|
|
|
@ -358,6 +358,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
|||
(Flake8Bugbear, "904") => (RuleGroup::Stable, rules::flake8_bugbear::rules::RaiseWithoutFromInsideExcept),
|
||||
(Flake8Bugbear, "905") => (RuleGroup::Stable, rules::flake8_bugbear::rules::ZipWithoutExplicitStrict),
|
||||
(Flake8Bugbear, "909") => (RuleGroup::Preview, rules::flake8_bugbear::rules::LoopIteratorMutation),
|
||||
(Flake8Bugbear, "911") => (RuleGroup::Preview, rules::flake8_bugbear::rules::BatchedWithoutExplicitStrict),
|
||||
|
||||
// flake8-blind-except
|
||||
(Flake8BlindExcept, "001") => (RuleGroup::Stable, rules::flake8_blind_except::rules::BlindExcept),
|
||||
|
|
|
@ -66,6 +66,7 @@ mod tests {
|
|||
#[test_case(Rule::ReturnInGenerator, Path::new("B901.py"))]
|
||||
#[test_case(Rule::LoopIteratorMutation, Path::new("B909.py"))]
|
||||
#[test_case(Rule::MutableContextvarDefault, Path::new("B039.py"))]
|
||||
#[test_case(Rule::BatchedWithoutExplicitStrict, Path::new("B911.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_bugbear::rules::is_infinite_iterable;
|
||||
use crate::settings::types::PythonVersion;
|
||||
use ruff_diagnostics::{Diagnostic, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `itertools.batched` calls without an explicit `strict` parameter.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// By default, if the length of the iterable is not divisible by
|
||||
/// the second argument to `itertools.batched`, the last batch
|
||||
/// will be shorter than the rest.
|
||||
///
|
||||
/// In Python 3.13, a `strict` parameter was added which allows controlling if the batches must be of uniform length.
|
||||
/// Pass `strict=True` to raise a `ValueError` if the batches are of non-uniform length.
|
||||
/// Otherwise, pass `strict=False` to make the intention explicit.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// itertools.batched(iterable, n)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead if the batches must be of uniform length:
|
||||
/// ```python
|
||||
/// itertools.batched(iterable, n, strict=True)
|
||||
/// ```
|
||||
///
|
||||
/// Or if the batches can be of non-uniform length:
|
||||
/// ```python
|
||||
/// itertools.batched(iterable, n, strict=False)
|
||||
/// ```
|
||||
///
|
||||
/// ## Known deviations
|
||||
/// Unlike the upstream `B911`, this rule will not report infinite iterators
|
||||
/// (e.g., `itertools.cycle(...)`).
|
||||
///
|
||||
/// ## Options
|
||||
/// - `target-version`
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `batched`](https://docs.python.org/3/library/itertools.html#batched)
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct BatchedWithoutExplicitStrict;
|
||||
|
||||
impl Violation for BatchedWithoutExplicitStrict {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::None;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"`itertools.batched()` without an explicit `strict` parameter".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Add an explicit `strict` parameter".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// B911
|
||||
pub(crate) fn batched_without_explicit_strict(checker: &mut Checker, call: &ExprCall) {
|
||||
if checker.settings.target_version < PythonVersion::Py313 {
|
||||
return;
|
||||
}
|
||||
|
||||
let semantic = checker.semantic();
|
||||
let (func, arguments) = (&call.func, &call.arguments);
|
||||
|
||||
let Some(qualified_name) = semantic.resolve_qualified_name(func) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !matches!(qualified_name.segments(), ["itertools", "batched"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if arguments.find_keyword("strict").is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(iterable) = arguments.find_positional(0) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if is_infinite_iterable(iterable, semantic) {
|
||||
return;
|
||||
}
|
||||
|
||||
let diagnostic = Diagnostic::new(BatchedWithoutExplicitStrict, call.range);
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
|
@ -2,6 +2,7 @@ pub(crate) use abstract_base_class::*;
|
|||
pub(crate) use assert_false::*;
|
||||
pub(crate) use assert_raises_exception::*;
|
||||
pub(crate) use assignment_to_os_environ::*;
|
||||
pub(crate) use batched_without_explicit_strict::*;
|
||||
pub(crate) use cached_instance_method::*;
|
||||
pub(crate) use duplicate_exceptions::*;
|
||||
pub(crate) use duplicate_value::*;
|
||||
|
@ -40,6 +41,7 @@ mod abstract_base_class;
|
|||
mod assert_false;
|
||||
mod assert_raises_exception;
|
||||
mod assignment_to_os_environ;
|
||||
mod batched_without_explicit_strict;
|
||||
mod cached_instance_method;
|
||||
mod duplicate_exceptions;
|
||||
mod duplicate_value;
|
||||
|
|
|
@ -17,7 +17,7 @@ use crate::fix::edits::add_argument;
|
|||
/// iterable. This can lead to subtle bugs.
|
||||
///
|
||||
/// Pass `strict=True` to raise a `ValueError` if the iterables are of
|
||||
/// non-uniform length. Alternatively, if the iterables are deliberately
|
||||
/// non-uniform length. Alternatively, if the iterables are deliberately of
|
||||
/// different lengths, pass `strict=False` to make the intention explicit.
|
||||
///
|
||||
/// ## Example
|
||||
|
@ -61,7 +61,7 @@ pub(crate) fn zip_without_explicit_strict(checker: &mut Checker, call: &ast::Exp
|
|||
.arguments
|
||||
.args
|
||||
.iter()
|
||||
.any(|arg| is_infinite_iterator(arg, semantic))
|
||||
.any(|arg| is_infinite_iterable(arg, semantic))
|
||||
{
|
||||
checker.diagnostics.push(
|
||||
Diagnostic::new(ZipWithoutExplicitStrict, call.range()).with_fix(Fix::applicable_edit(
|
||||
|
@ -89,7 +89,7 @@ pub(crate) fn zip_without_explicit_strict(checker: &mut Checker, call: &ast::Exp
|
|||
|
||||
/// Return `true` if the [`Expr`] appears to be an infinite iterator (e.g., a call to
|
||||
/// `itertools.cycle` or similar).
|
||||
fn is_infinite_iterator(arg: &Expr, semantic: &SemanticModel) -> bool {
|
||||
pub(crate) fn is_infinite_iterable(arg: &Expr, semantic: &SemanticModel) -> bool {
|
||||
let Expr::Call(ast::ExprCall {
|
||||
func,
|
||||
arguments: Arguments { args, keywords, .. },
|
||||
|
|
|
@ -0,0 +1,169 @@
|
|||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
B911.py:5:1: B911 `itertools.batched()` without an explicit `strict` parameter
|
||||
|
|
||||
4 | # Errors
|
||||
5 | batched(range(3), 1)
|
||||
| ^^^^^^^^^^^^^^^^^^^^ B911
|
||||
6 | batched("abc", 2)
|
||||
7 | batched([i for i in range(42)], some_n)
|
||||
|
|
||||
= help: Add an explicit `strict` parameter
|
||||
|
||||
B911.py:6:1: B911 `itertools.batched()` without an explicit `strict` parameter
|
||||
|
|
||||
4 | # Errors
|
||||
5 | batched(range(3), 1)
|
||||
6 | batched("abc", 2)
|
||||
| ^^^^^^^^^^^^^^^^^ B911
|
||||
7 | batched([i for i in range(42)], some_n)
|
||||
8 | batched((foo for foo in cycle()))
|
||||
|
|
||||
= help: Add an explicit `strict` parameter
|
||||
|
||||
B911.py:7:1: B911 `itertools.batched()` without an explicit `strict` parameter
|
||||
|
|
||||
5 | batched(range(3), 1)
|
||||
6 | batched("abc", 2)
|
||||
7 | batched([i for i in range(42)], some_n)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911
|
||||
8 | batched((foo for foo in cycle()))
|
||||
9 | batched(itertools.batched([1, 2, 3], strict=True))
|
||||
|
|
||||
= help: Add an explicit `strict` parameter
|
||||
|
||||
B911.py:8:1: B911 `itertools.batched()` without an explicit `strict` parameter
|
||||
|
|
||||
6 | batched("abc", 2)
|
||||
7 | batched([i for i in range(42)], some_n)
|
||||
8 | batched((foo for foo in cycle()))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911
|
||||
9 | batched(itertools.batched([1, 2, 3], strict=True))
|
||||
|
|
||||
= help: Add an explicit `strict` parameter
|
||||
|
||||
B911.py:9:1: B911 `itertools.batched()` without an explicit `strict` parameter
|
||||
|
|
||||
7 | batched([i for i in range(42)], some_n)
|
||||
8 | batched((foo for foo in cycle()))
|
||||
9 | batched(itertools.batched([1, 2, 3], strict=True))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911
|
||||
10 |
|
||||
11 | # Errors (limited iterators).
|
||||
|
|
||||
= help: Add an explicit `strict` parameter
|
||||
|
||||
B911.py:12:1: B911 `itertools.batched()` without an explicit `strict` parameter
|
||||
|
|
||||
11 | # Errors (limited iterators).
|
||||
12 | batched(repeat(1, 1))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ B911
|
||||
13 | batched(repeat(1, times=4))
|
||||
|
|
||||
= help: Add an explicit `strict` parameter
|
||||
|
||||
B911.py:13:1: B911 `itertools.batched()` without an explicit `strict` parameter
|
||||
|
|
||||
11 | # Errors (limited iterators).
|
||||
12 | batched(repeat(1, 1))
|
||||
13 | batched(repeat(1, times=4))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911
|
||||
14 |
|
||||
15 | # No fix
|
||||
|
|
||||
= help: Add an explicit `strict` parameter
|
||||
|
||||
B911.py:16:1: B911 `itertools.batched()` without an explicit `strict` parameter
|
||||
|
|
||||
15 | # No fix
|
||||
16 | batched([], **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ B911
|
||||
17 |
|
||||
18 | # No errors
|
||||
|
|
||||
= help: Add an explicit `strict` parameter
|
||||
|
||||
B911.py:35:1: B911 `itertools.batched()` without an explicit `strict` parameter
|
||||
|
|
||||
34 | # Errors
|
||||
35 | itertools.batched(range(3), 1)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911
|
||||
36 | itertools.batched("abc", 2)
|
||||
37 | itertools.batched([i for i in range(42)], some_n)
|
||||
|
|
||||
= help: Add an explicit `strict` parameter
|
||||
|
||||
B911.py:36:1: B911 `itertools.batched()` without an explicit `strict` parameter
|
||||
|
|
||||
34 | # Errors
|
||||
35 | itertools.batched(range(3), 1)
|
||||
36 | itertools.batched("abc", 2)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911
|
||||
37 | itertools.batched([i for i in range(42)], some_n)
|
||||
38 | itertools.batched((foo for foo in cycle()))
|
||||
|
|
||||
= help: Add an explicit `strict` parameter
|
||||
|
||||
B911.py:37:1: B911 `itertools.batched()` without an explicit `strict` parameter
|
||||
|
|
||||
35 | itertools.batched(range(3), 1)
|
||||
36 | itertools.batched("abc", 2)
|
||||
37 | itertools.batched([i for i in range(42)], some_n)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911
|
||||
38 | itertools.batched((foo for foo in cycle()))
|
||||
39 | itertools.batched(itertools.batched([1, 2, 3], strict=True))
|
||||
|
|
||||
= help: Add an explicit `strict` parameter
|
||||
|
||||
B911.py:38:1: B911 `itertools.batched()` without an explicit `strict` parameter
|
||||
|
|
||||
36 | itertools.batched("abc", 2)
|
||||
37 | itertools.batched([i for i in range(42)], some_n)
|
||||
38 | itertools.batched((foo for foo in cycle()))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911
|
||||
39 | itertools.batched(itertools.batched([1, 2, 3], strict=True))
|
||||
|
|
||||
= help: Add an explicit `strict` parameter
|
||||
|
||||
B911.py:39:1: B911 `itertools.batched()` without an explicit `strict` parameter
|
||||
|
|
||||
37 | itertools.batched([i for i in range(42)], some_n)
|
||||
38 | itertools.batched((foo for foo in cycle()))
|
||||
39 | itertools.batched(itertools.batched([1, 2, 3], strict=True))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911
|
||||
40 |
|
||||
41 | # Errors (limited iterators).
|
||||
|
|
||||
= help: Add an explicit `strict` parameter
|
||||
|
||||
B911.py:42:1: B911 `itertools.batched()` without an explicit `strict` parameter
|
||||
|
|
||||
41 | # Errors (limited iterators).
|
||||
42 | itertools.batched(repeat(1, 1))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911
|
||||
43 | itertools.batched(repeat(1, times=4))
|
||||
|
|
||||
= help: Add an explicit `strict` parameter
|
||||
|
||||
B911.py:43:1: B911 `itertools.batched()` without an explicit `strict` parameter
|
||||
|
|
||||
41 | # Errors (limited iterators).
|
||||
42 | itertools.batched(repeat(1, 1))
|
||||
43 | itertools.batched(repeat(1, times=4))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911
|
||||
44 |
|
||||
45 | # No fix
|
||||
|
|
||||
= help: Add an explicit `strict` parameter
|
||||
|
||||
B911.py:46:1: B911 `itertools.batched()` without an explicit `strict` parameter
|
||||
|
|
||||
45 | # No fix
|
||||
46 | itertools.batched([], **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B911
|
||||
47 |
|
||||
48 | # No errors
|
||||
|
|
||||
= help: Add an explicit `strict` parameter
|
Loading…
Add table
Add a link
Reference in a new issue