From 6ba76a4e508463916a531b18bd943e59495179f9 Mon Sep 17 00:00:00 2001 From: "elric.lim" Date: Sat, 3 Oct 2026 01:48:38 +0900 Subject: [PATCH 1/2] fix: modernize TypeForm imports on Python 3.15 --- crates/ruff_linter/src/rules/pyupgrade/mod.rs | 41 ++++++++++++++++++- .../pyupgrade/rules/deprecated_import.rs | 8 ++++ 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index b366d68895c145..9456742739197f 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -18,7 +18,8 @@ mod tests { use crate::registry::Rule; use crate::rules::{isort, pyupgrade}; use crate::settings::types::PreviewMode; - use crate::test::{test_path, test_snippet}; + use crate::source_kind::SourceKind; + use crate::test::{test_contents, test_path, test_snippet}; use crate::{assert_diagnostics, assert_diagnostics_diff, settings}; #[test_case(Rule::ConvertNamedTupleFunctionalToClass, Path::new("UP014.py"))] @@ -128,6 +129,44 @@ mod tests { Ok(()) } + #[test_case(PythonVersion::PY314)] + #[test_case(PythonVersion::PY315)] + fn deprecated_import_type_form(target_version: PythonVersion) { + for (source, expected) in [ + ( + "from typing_extensions import TypeForm\n", + "from typing import TypeForm\n", + ), + ( + "from typing_extensions import TypeForm as TF\n", + "from typing import TypeForm as TF\n", + ), + ( + "from typing_extensions import TypeForm, Sentinel\n", + "from typing_extensions import Sentinel\nfrom typing import TypeForm\n", + ), + ] { + let source_kind = SourceKind::Python { + code: source.to_string(), + is_stub: false, + }; + let (diagnostics, fixed) = test_contents( + &source_kind, + Path::new("example.py"), + &settings::LinterSettings::for_rule(Rule::DeprecatedImport) + .with_target_version(target_version), + ); + + if target_version >= PythonVersion::PY315 { + assert_eq!(diagnostics.len(), 1); + assert_eq!(fixed.source_code(), expected); + } else { + assert!(diagnostics.is_empty()); + assert_eq!(fixed.source_code(), source); + } + } + } + #[test_case(PythonVersion::PY37)] #[test_case(PythonVersion::PY312)] #[test_case(PythonVersion::PY313)] diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_import.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_import.rs index 5939a2e34ffaca..8a82b29574d3e6 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_import.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/deprecated_import.rs @@ -415,6 +415,11 @@ const TYPING_EXTENSIONS_TO_TYPES_313: &[&str] = &["CapsuleType"]; // Members of typing_extensions that were moved to `warnings` const TYPING_EXTENSIONS_TO_WARNINGS_313: &[&str] = &["deprecated"]; +// Python 3.15+ + +// Members of `typing_extensions` that were moved to `typing`. +const TYPING_EXTENSIONS_TO_TYPING_315: &[&str] = &["TypeForm"]; + struct ImportReplacer<'a> { import_from_stmt: &'a StmtImportFrom, module: &'a str, @@ -551,6 +556,9 @@ impl<'a> ImportReplacer<'a> { if self.version >= PythonVersion::PY313 { typing_extensions_to_typing.extend(TYPING_EXTENSIONS_TO_TYPING_313); } + if self.version >= PythonVersion::PY315 { + typing_extensions_to_typing.extend(TYPING_EXTENSIONS_TO_TYPING_315); + } if let Some(operation) = self.try_replace(&typing_extensions_to_typing, "typing") { operations.push(operation); } From 0bb23c00916730a43eb167dee8f9217c5850d241 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 5 Oct 2026 13:35:53 -0400 Subject: [PATCH 2/2] mdtest --- .../mdtest/pyupgrade/deprecated-import.md | 34 +++++++++++++++ crates/ruff_linter/src/rules/pyupgrade/mod.rs | 41 +------------------ 2 files changed, 35 insertions(+), 40 deletions(-) diff --git a/crates/ruff_linter/resources/mdtest/pyupgrade/deprecated-import.md b/crates/ruff_linter/resources/mdtest/pyupgrade/deprecated-import.md index 0584a5a3418cd7..85494f91d909f0 100644 --- a/crates/ruff_linter/resources/mdtest/pyupgrade/deprecated-import.md +++ b/crates/ruff_linter/resources/mdtest/pyupgrade/deprecated-import.md @@ -50,3 +50,37 @@ help: Import from `collections.abc` 4 + lazy from collections.abc import Iterable | ``` + +## `TypeForm` + +On Python 3.15 and later, `TypeForm` can be imported from `typing`. + +```py +from typing_extensions import TypeForm # snapshot: deprecated-import +``` + +```snapshot +error[UP035]: Import from `typing` instead: `TypeForm` + --> src/mdtest_snippet.py:1:1 + | +1 | from typing_extensions import TypeForm # snapshot: deprecated-import + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +help: Import from `typing` + | + - from typing_extensions import TypeForm # snapshot: deprecated-import +1 + from typing import TypeForm # snapshot: deprecated-import + | +``` + +## `TypeForm` before Python 3.15 + +```toml +target-version = "py314" + +[lint] +select = ["UP035"] +``` + +```py +from typing_extensions import TypeForm # no diagnostic +``` diff --git a/crates/ruff_linter/src/rules/pyupgrade/mod.rs b/crates/ruff_linter/src/rules/pyupgrade/mod.rs index 9456742739197f..b366d68895c145 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/mod.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/mod.rs @@ -18,8 +18,7 @@ mod tests { use crate::registry::Rule; use crate::rules::{isort, pyupgrade}; use crate::settings::types::PreviewMode; - use crate::source_kind::SourceKind; - use crate::test::{test_contents, test_path, test_snippet}; + use crate::test::{test_path, test_snippet}; use crate::{assert_diagnostics, assert_diagnostics_diff, settings}; #[test_case(Rule::ConvertNamedTupleFunctionalToClass, Path::new("UP014.py"))] @@ -129,44 +128,6 @@ mod tests { Ok(()) } - #[test_case(PythonVersion::PY314)] - #[test_case(PythonVersion::PY315)] - fn deprecated_import_type_form(target_version: PythonVersion) { - for (source, expected) in [ - ( - "from typing_extensions import TypeForm\n", - "from typing import TypeForm\n", - ), - ( - "from typing_extensions import TypeForm as TF\n", - "from typing import TypeForm as TF\n", - ), - ( - "from typing_extensions import TypeForm, Sentinel\n", - "from typing_extensions import Sentinel\nfrom typing import TypeForm\n", - ), - ] { - let source_kind = SourceKind::Python { - code: source.to_string(), - is_stub: false, - }; - let (diagnostics, fixed) = test_contents( - &source_kind, - Path::new("example.py"), - &settings::LinterSettings::for_rule(Rule::DeprecatedImport) - .with_target_version(target_version), - ); - - if target_version >= PythonVersion::PY315 { - assert_eq!(diagnostics.len(), 1); - assert_eq!(fixed.source_code(), expected); - } else { - assert!(diagnostics.is_empty()); - assert_eq!(fixed.source_code(), source); - } - } - } - #[test_case(PythonVersion::PY37)] #[test_case(PythonVersion::PY312)] #[test_case(PythonVersion::PY313)]