From da16e00751e39a3bef0654a0fa57d39433c24d6c Mon Sep 17 00:00:00 2001 From: Igor Drokin <41319097+LyricalToxic@users.noreply.github.com> Date: Mon, 23 Jun 2025 23:01:55 +0300 Subject: [PATCH] [`pyupgrade`] Extend version detection to include `sys.version_info.major` (`UP036`) (#18633) ## Summary Resolves #18165 Added pattern `["sys", "version_info", "major"]` to the existing matches for `sys.version_info` to ensure consistent handling of both the base object and its major version attribute. ## Test Plan `cargo nextest run` and `cargo insta test` --------- Co-authored-by: Brent Westbrook --- .../test/fixtures/pyupgrade/UP036_5.py | 44 +++++++ .../pyupgrade/rules/outdated_version_block.rs | 25 ++-- ...__rules__pyupgrade__tests__UP036_5.py.snap | 120 ++++++++++++++++++ 3 files changed, 181 insertions(+), 8 deletions(-) diff --git a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP036_5.py b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP036_5.py index 697c84a804..68e3f167b6 100644 --- a/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP036_5.py +++ b/crates/ruff_linter/resources/test/fixtures/pyupgrade/UP036_5.py @@ -71,3 +71,47 @@ if sys.version_info <= (3, 14, 0): if sys.version_info <= (3, 14, 15): print() + +# https://github.com/astral-sh/ruff/issues/18165 + +if sys.version_info.major >= 3: + print("3") +else: + print("2") + +if sys.version_info.major > 3: + print("3") +else: + print("2") + +if sys.version_info.major <= 3: + print("3") +else: + print("2") + +if sys.version_info.major < 3: + print("3") +else: + print("2") + +if sys.version_info.major == 3: + print("3") +else: + print("2") + +# Semantically incorrect, skip fixing + +if sys.version_info.major[1] > 3: + print(3) +else: + print(2) + +if sys.version_info.major > (3, 13): + print(3) +else: + print(2) + +if sys.version_info.major[:2] > (3, 13): + print(3) +else: + print(2) diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs index c8a44d8d5c..52e4cd9771 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/outdated_version_block.rs @@ -14,6 +14,7 @@ use crate::checkers::ast::Checker; use crate::fix::edits::{adjust_indentation, delete_stmt}; use crate::{Edit, Fix, FixAvailability, Violation}; use ruff_python_ast::PythonVersion; +use ruff_python_semantic::SemanticModel; /// ## What it does /// Checks for conditional blocks gated on `sys.version_info` comparisons @@ -103,14 +104,7 @@ pub(crate) fn outdated_version_block(checker: &Checker, stmt_if: &StmtIf) { continue; }; - // Detect `sys.version_info`, along with slices (like `sys.version_info[:2]`). - if !checker - .semantic() - .resolve_qualified_name(map_subscript(left)) - .is_some_and(|qualified_name| { - matches!(qualified_name.segments(), ["sys", "version_info"]) - }) - { + if !is_valid_version_info(checker.semantic(), left) { continue; } @@ -456,6 +450,21 @@ fn extract_version(elts: &[Expr]) -> Option> { Some(version) } +/// Returns `true` if the expression is related to `sys.version_info`. +/// +/// This includes: +/// - Direct access: `sys.version_info` +/// - Subscript access: `sys.version_info[:2]`, `sys.version_info[0]` +/// - Major version attribute: `sys.version_info.major` +fn is_valid_version_info(semantic: &SemanticModel, left: &Expr) -> bool { + semantic + .resolve_qualified_name(map_subscript(left)) + .is_some_and(|name| matches!(name.segments(), ["sys", "version_info"])) + || semantic + .resolve_qualified_name(left) + .is_some_and(|name| matches!(name.segments(), ["sys", "version_info", "major"])) +} + #[cfg(test)] mod tests { use test_case::test_case; diff --git a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP036_5.py.snap b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP036_5.py.snap index efd113261f..b1c24a6ba9 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP036_5.py.snap +++ b/crates/ruff_linter/src/rules/pyupgrade/snapshots/ruff_linter__rules__pyupgrade__tests__UP036_5.py.snap @@ -157,3 +157,123 @@ UP036_5.py:48:24: UP036 Version specifier is invalid | ^^^^^^^^^^^^^^^ UP036 49 | print() | + +UP036_5.py:77:4: UP036 [*] Version block is outdated for minimum Python version + | +75 | # https://github.com/astral-sh/ruff/issues/18165 +76 | +77 | if sys.version_info.major >= 3: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP036 +78 | print("3") +79 | else: + | + = help: Remove outdated version block + +ℹ Unsafe fix +74 74 | +75 75 | # https://github.com/astral-sh/ruff/issues/18165 +76 76 | +77 |-if sys.version_info.major >= 3: +78 |- print("3") +79 |-else: +80 |- print("2") + 77 |+print("3") +81 78 | +82 79 | if sys.version_info.major > 3: +83 80 | print("3") + +UP036_5.py:82:4: UP036 [*] Version block is outdated for minimum Python version + | +80 | print("2") +81 | +82 | if sys.version_info.major > 3: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ UP036 +83 | print("3") +84 | else: + | + = help: Remove outdated version block + +ℹ Unsafe fix +79 79 | else: +80 80 | print("2") +81 81 | +82 |-if sys.version_info.major > 3: +83 |- print("3") +84 |-else: +85 |- print("2") + 82 |+print("2") +86 83 | +87 84 | if sys.version_info.major <= 3: +88 85 | print("3") + +UP036_5.py:87:4: UP036 [*] Version block is outdated for minimum Python version + | +85 | print("2") +86 | +87 | if sys.version_info.major <= 3: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP036 +88 | print("3") +89 | else: + | + = help: Remove outdated version block + +ℹ Unsafe fix +84 84 | else: +85 85 | print("2") +86 86 | +87 |-if sys.version_info.major <= 3: +88 |- print("3") +89 |-else: +90 |- print("2") + 87 |+print("3") +91 88 | +92 89 | if sys.version_info.major < 3: +93 90 | print("3") + +UP036_5.py:92:4: UP036 [*] Version block is outdated for minimum Python version + | +90 | print("2") +91 | +92 | if sys.version_info.major < 3: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ UP036 +93 | print("3") +94 | else: + | + = help: Remove outdated version block + +ℹ Unsafe fix +89 89 | else: +90 90 | print("2") +91 91 | +92 |-if sys.version_info.major < 3: +93 |- print("3") +94 |-else: +95 |- print("2") + 92 |+print("2") +96 93 | +97 94 | if sys.version_info.major == 3: +98 95 | print("3") + +UP036_5.py:97:4: UP036 [*] Version block is outdated for minimum Python version + | +95 | print("2") +96 | +97 | if sys.version_info.major == 3: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP036 +98 | print("3") +99 | else: + | + = help: Remove outdated version block + +ℹ Unsafe fix +94 94 | else: +95 95 | print("2") +96 96 | +97 |-if sys.version_info.major == 3: +98 |- print("3") +99 |-else: +100 |- print("2") + 97 |+print("3") +101 98 | +102 99 | # Semantically incorrect, skip fixing +103 100 |