Skip to content

Avoid infinite loop between I002 and PYI025 #23352

Merged
ntBre merged 2 commits intoastral-sh:mainfrom
kar-ganap:fix/issue-20891
Feb 24, 2026
Merged

Avoid infinite loop between I002 and PYI025 #23352
ntBre merged 2 commits intoastral-sh:mainfrom
kar-ganap:fix/issue-20891

Conversation

@kar-ganap
Copy link
Contributor

Summary

Fixes #20891.

When lint.isort.required-imports includes from collections.abc import Set (unaliased) and PYI025 is enabled, the two rules conflict: I002 inserts the unaliased import, while PYI025 demands it be aliased as AbstractSet. This causes an infinite autofix loop ("Failed to converge after 100 iterations").

Rather than patching the import-insertion logic at the rule level, this PR rejects the contradictory configuration at startup in Configuration::into_settings(), following the existing pattern established by conflicting_import_settings(). The error message explains the conflict and suggests two resolutions: alias the required import as AbstractSet, or disable PYI025.

Prior art

PR #21115 took a different approach (modifying add_required_imports.rs and imports.rs to relax alias matching). As noted in review, the better fix is to "reject this configuration entirely at an earlier stage" since it doesn't make sense to simultaneously require an unaliased import and forbid it. This PR implements that suggestion directly.

Test plan

  • 3 unit tests: conflicting config → error, aliased as AbstractSet → ok, PYI025 not enabled → ok
  • Reproduction case: ruff check --config ruff.toml --unsafe-fixes --fix now emits a clear configuration error instead of entering the infinite loop
  • cargo test -p ruff_workspace — all 26 tests pass

Fixes astral-sh#20891.

When `lint.isort.required-imports` includes `from collections.abc import
Set` (unaliased) and PYI025 is enabled, the two rules conflict: I002
inserts the unaliased import, while PYI025 demands it be aliased as
`AbstractSet`. This causes an infinite autofix loop.

Rather than patching the import-insertion logic, reject this
contradictory configuration at startup in `Configuration::into_settings`,
following the existing pattern of `conflicting_import_settings()`. The
new `conflicting_required_import_pyi025()` function checks for the
specific conflict and emits a clear error message with a suggested fix.

Aliasing the required import as `AbstractSet` (satisfying both rules)
or disabling PYI025 are both accepted configurations.
@astral-sh-bot
Copy link

astral-sh-bot bot commented Feb 17, 2026

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

@kar-ganap
Copy link
Contributor Author

Gentle ping — this has been open since Feb 16 with clean CI and zero ecosystem impact. The approach follows the pattern suggested in #20891 (reject conflicting configuration at startup rather than working around it at the rule level). Would appreciate a review when someone has a chance.

@ntBre ntBre added bug Something isn't working fixes Related to suggested fixes for violations labels Feb 23, 2026
Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this looks good overall. I just had one testing suggestion.

Ok(())
}

mod pyi025_conflict {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just add CLI tests like these:

#[test]
fn flake8_import_convention_unused_aliased_import() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(r#"lint.isort.required-imports = ["import pandas"]"#)
.args(["--select", "I002,ICN001,F401"])
.args(["--stdin-filename", "test.py"])
.arg("--unsafe-fixes")
.arg("--fix")
.arg("-")
.pass_stdin("1")
);
}
#[test]
fn flake8_import_convention_unused_aliased_import_no_conflict() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.arg("--config")
.arg(r#"lint.isort.required-imports = ["import pandas as pd"]"#)
.args(["--select", "I002,ICN001,F401"])
.args(["--stdin-filename", "test.py"])
.arg("--unsafe-fixes")
.arg("--fix")
.arg("-")
.pass_stdin("1")
);
}

That will also get us snapshots to verify the full contents of the error message.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — replaced the unit tests with CLI snapshot tests in crates/ruff/tests/cli/lint.rs, following the pattern from the existing flake8_import_convention_unused_aliased_import tests. Three tests:

  1. required_import_set_conflicts_with_pyi025 — unaliased Set + PYI025 → exit code 2 with error message
  2. required_import_set_aliased_as_abstract_set_no_conflict — aliased as AbstractSet → no config error
  3. required_import_set_without_pyi025_no_conflict — PYI025 not selected → no config error

Move PYI025 conflict tests from ruff_workspace unit tests to CLI
integration tests using assert_cmd_snapshot!, matching the pattern
used by the existing flake8-import-conventions conflict tests.
Copy link
Contributor

@ntBre ntBre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@ntBre ntBre changed the title Reject conflicting I002 + PYI025 configuration at startup Avoid infinite loop between I002 and PYI025 Feb 24, 2026
@ntBre ntBre merged commit df313c2 into astral-sh:main Feb 24, 2026
42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working fixes Related to suggested fixes for violations

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Infinite loop] F401, I002, and PYI025

2 participants