diff --git a/crates/ruff/src/autofix/codemods.rs b/crates/ruff/src/autofix/codemods.rs index 0797818407c573..389107f787f192 100644 --- a/crates/ruff/src/autofix/codemods.rs +++ b/crates/ruff/src/autofix/codemods.rs @@ -123,3 +123,88 @@ pub(crate) fn remove_imports<'a>( Ok(Some(state.to_string())) } + +/// Given an import statement, remove any imports that are not specified in the `imports` slice. +/// +/// Returns the modified import statement. +pub(crate) fn retain_imports( + imports: &[&str], + stmt: &Stmt, + locator: &Locator, + stylist: &Stylist, +) -> Result { + let module_text = locator.slice(stmt.range()); + let mut tree = match_statement(module_text)?; + + let Statement::Simple(body) = &mut tree else { + bail!("Expected Statement::Simple"); + }; + + let (aliases, import_module) = match body.body.first_mut() { + Some(SmallStatement::Import(import_body)) => (&mut import_body.names, None), + Some(SmallStatement::ImportFrom(import_body)) => { + if let ImportNames::Aliases(names) = &mut import_body.names { + ( + names, + Some((&import_body.relative, import_body.module.as_ref())), + ) + } else { + bail!("Expected: ImportNames::Aliases"); + } + } + _ => bail!("Expected: SmallStatement::ImportFrom | SmallStatement::Import"), + }; + + // Preserve the trailing comma (or not) from the last entry. + let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone()); + + aliases.retain(|alias| { + imports.iter().any(|import| { + let full_name = match import_module { + Some((relative, module)) => { + let module = module.map(compose_module_path); + let member = compose_module_path(&alias.name); + let mut full_name = String::with_capacity( + relative.len() + module.as_ref().map_or(0, String::len) + member.len() + 1, + ); + for _ in 0..relative.len() { + full_name.push('.'); + } + if let Some(module) = module { + full_name.push_str(&module); + full_name.push('.'); + } + full_name.push_str(&member); + full_name + } + None => compose_module_path(&alias.name), + }; + full_name == *import + }) + }); + + // But avoid destroying any trailing comments. + if let Some(alias) = aliases.last_mut() { + let has_comment = if let Some(comma) = &alias.comma { + match &comma.whitespace_after { + ParenthesizableWhitespace::SimpleWhitespace(_) => false, + ParenthesizableWhitespace::ParenthesizedWhitespace(whitespace) => { + whitespace.first_line.comment.is_some() + } + } + } else { + false + }; + if !has_comment { + alias.comma = trailing_comma; + } + } + + let mut state = CodegenState { + default_newline: &stylist.line_ending(), + default_indent: stylist.indentation(), + ..CodegenState::default() + }; + tree.codegen(&mut state); + Ok(state.to_string()) +} diff --git a/crates/ruff/src/autofix/edits.rs b/crates/ruff/src/autofix/edits.rs index 7361935305e064..158c59b8ec2631 100644 --- a/crates/ruff/src/autofix/edits.rs +++ b/crates/ruff/src/autofix/edits.rs @@ -61,7 +61,7 @@ pub(crate) fn delete_stmt( } } -/// Generate a `Fix` to remove any unused imports from an `import` statement. +/// Generate a `Fix` to remove the specified imports from an `import` statement. pub(crate) fn remove_unused_imports<'a>( unused_imports: impl Iterator, stmt: &Stmt, diff --git a/crates/ruff/src/importer/insertion.rs b/crates/ruff/src/importer/insertion.rs index c66d663aade6a4..7547d44c8ee56c 100644 --- a/crates/ruff/src/importer/insertion.rs +++ b/crates/ruff/src/importer/insertion.rs @@ -1,6 +1,4 @@ //! Insert statements into Python code. -#![allow(dead_code)] - use ruff_text_size::TextSize; use rustpython_parser::ast::{Ranged, Stmt}; use rustpython_parser::{lexer, Mode, Tok}; diff --git a/crates/ruff/src/importer/mod.rs b/crates/ruff/src/importer/mod.rs index df980012b6458b..8a222c77a8ce9d 100644 --- a/crates/ruff/src/importer/mod.rs +++ b/crates/ruff/src/importer/mod.rs @@ -7,10 +7,12 @@ use libcst_native::{Codegen, CodegenState, ImportAlias, Name, NameOrAttribute}; use ruff_text_size::TextSize; use rustpython_parser::ast::{self, Ranged, Stmt, Suite}; +use crate::autofix; use ruff_diagnostics::Edit; use ruff_python_ast::imports::{AnyImport, Import, ImportFrom}; use ruff_python_ast::source_code::{Locator, Stylist}; use ruff_python_semantic::model::SemanticModel; +use ruff_textwrap::indent; use crate::cst::matchers::{match_aliases, match_import_from, match_statement}; use crate::importer::insertion::Insertion; @@ -73,6 +75,52 @@ impl<'a> Importer<'a> { } } + /// Move an existing import into a `TYPE_CHECKING` block. + /// + /// If there are no existing `TYPE_CHECKING` blocks, a new one will be added at the top + /// of the file. Otherwise, it will be added after the most recent top-level + /// `TYPE_CHECKING` block. + pub(crate) fn to_typing_import( + &self, + import: &StmtImport, + at: TextSize, + semantic_model: &SemanticModel, + ) -> Result<(Edit, Edit)> { + // Generate the modified import statement. + let content = autofix::codemods::retain_imports( + &[import.full_name], + import.stmt, + self.locator, + self.stylist, + )?; + + // Import the `TYPE_CHECKING` symbol from the typing module. + let (type_checking_edit, type_checking) = self.get_or_import_symbol( + &ImportRequest::import_from("typing", "TYPE_CHECKING"), + at, + semantic_model, + )?; + + // Add the import to a `TYPE_CHECKING` block. + let add_import_edit = if let Some(block) = self.preceding_type_checking_block(at) { + // Add the import to the `TYPE_CHECKING` block. + self.add_to_type_checking_block(&content, block.start()) + } else { + // Add the import to a new `TYPE_CHECKING` block. + self.add_type_checking_block( + &format!( + "{}if {type_checking}:{}{}", + self.stylist.line_ending().as_str(), + self.stylist.line_ending().as_str(), + indent(&content, self.stylist.indentation()) + ), + at, + )? + }; + + Ok((type_checking_edit, add_import_edit)) + } + /// Generate an [`Edit`] to reference the given symbol. Returns the [`Edit`] necessary to make /// the symbol available in the current scope along with the bound name of the symbol. /// @@ -204,14 +252,6 @@ impl<'a> Importer<'a> { } } - /// Return the import statement that precedes the given position, if any. - fn preceding_import(&self, at: TextSize) -> Option<&Stmt> { - self.runtime_imports - .partition_point(|stmt| stmt.start() < at) - .checked_sub(1) - .map(|idx| self.runtime_imports[idx]) - } - /// Return the top-level [`Stmt`] that imports the given module using `Stmt::ImportFrom` /// preceding the given position, if any. fn find_import_from(&self, module: &str, at: TextSize) -> Option<&Stmt> { @@ -258,6 +298,45 @@ impl<'a> Importer<'a> { statement.codegen(&mut state); Ok(Edit::range_replacement(state.to_string(), stmt.range())) } + + /// Add a `TYPE_CHECKING` block to the given module. + fn add_type_checking_block(&self, content: &str, at: TextSize) -> Result { + let insertion = if let Some(stmt) = self.preceding_import(at) { + // Insert after the last top-level import. + Insertion::end_of_statement(stmt, self.locator, self.stylist) + } else { + // Insert at the start of the file. + Insertion::start_of_file(self.python_ast, self.locator, self.stylist) + }; + if insertion.is_inline() { + Err(anyhow::anyhow!( + "Cannot insert `TYPE_CHECKING` block inline" + )) + } else { + Ok(insertion.into_edit(content)) + } + } + + /// Add an import statement to an existing `TYPE_CHECKING` block. + fn add_to_type_checking_block(&self, content: &str, at: TextSize) -> Edit { + Insertion::start_of_block(at, self.locator, self.stylist).into_edit(content) + } + + /// Return the import statement that precedes the given position, if any. + fn preceding_import(&self, at: TextSize) -> Option<&'a Stmt> { + self.runtime_imports + .partition_point(|stmt| stmt.start() < at) + .checked_sub(1) + .map(|idx| self.runtime_imports[idx]) + } + + /// Return the import statement that precedes the given position, if any. + fn preceding_type_checking_block(&self, at: TextSize) -> Option<&'a Stmt> { + self.type_checking_blocks + .partition_point(|stmt| stmt.start() < at) + .checked_sub(1) + .map(|idx| self.type_checking_blocks[idx]) + } } #[derive(Debug)] @@ -301,6 +380,14 @@ impl<'a> ImportRequest<'a> { } } +/// An existing module or member import, located within an import statement. +pub(crate) struct StmtImport<'a> { + /// The import statement. + pub(crate) stmt: &'a Stmt, + /// The "full name" of the imported module or member. + pub(crate) full_name: &'a str, +} + /// The result of an [`Importer::get_or_import_symbol`] call. #[derive(Debug)] pub(crate) enum ResolutionError { diff --git a/crates/ruff/src/rules/flake8_type_checking/mod.rs b/crates/ruff/src/rules/flake8_type_checking/mod.rs index 79a9e6f2b5af5c..9f18ccded74d86 100644 --- a/crates/ruff/src/rules/flake8_type_checking/mod.rs +++ b/crates/ruff/src/rules/flake8_type_checking/mod.rs @@ -11,8 +11,8 @@ mod tests { use anyhow::Result; use test_case::test_case; - use crate::registry::Rule; - use crate::test::test_path; + use crate::registry::{Linter, Rule}; + use crate::test::{test_path, test_snippet}; use crate::{assert_messages, settings}; #[test_case(Rule::TypingOnlyFirstPartyImport, Path::new("TCH001.py"))] @@ -134,4 +134,142 @@ mod tests { assert_messages!(snapshot, diagnostics); Ok(()) } + + #[test_case( + r#" + from __future__ import annotations + + import pandas as pd + + def f(x: pd.DataFrame): + pass + "#, + "no_typing_import" + )] + #[test_case( + r#" + from __future__ import annotations + + from typing import TYPE_CHECKING + + import pandas as pd + + def f(x: pd.DataFrame): + pass + "#, + "typing_import_before_package_import" + )] + #[test_case( + r#" + from __future__ import annotations + + import pandas as pd + + from typing import TYPE_CHECKING + + def f(x: pd.DataFrame): + pass + "#, + "typing_import_after_package_import" + )] + #[test_case( + r#" + from __future__ import annotations + + import pandas as pd + + def f(x: pd.DataFrame): + pass + + from typing import TYPE_CHECKING + "#, + "typing_import_after_usage" + )] + #[test_case( + r#" + from __future__ import annotations + + from typing import TYPE_CHECKING + + import pandas as pd + + if TYPE_CHECKING: + import os + + def f(x: pd.DataFrame): + pass + "#, + "type_checking_block_own_line" + )] + #[test_case( + r#" + from __future__ import annotations + + from typing import TYPE_CHECKING + + import pandas as pd + + if TYPE_CHECKING: import os + + def f(x: pd.DataFrame): + pass + "#, + "type_checking_block_inline" + )] + #[test_case( + r#" + from __future__ import annotations + + from typing import TYPE_CHECKING + + import pandas as pd + + def f(x: pd.DataFrame): + pass + + if TYPE_CHECKING: + import os + "#, + "type_checking_block_after_usage" + )] + #[test_case( + r#" + from __future__ import annotations + + from pandas import ( + DataFrame, # DataFrame + Series, # Series + ) + + def f(x: DataFrame): + pass + "#, + "import_from" + )] + #[test_case( + r#" + from __future__ import annotations + + from typing import TYPE_CHECKING + + from pandas import ( + DataFrame, # DataFrame + Series, # Series + ) + + if TYPE_CHECKING: + import os + + def f(x: DataFrame): + pass + "#, + "import_from_type_checking_block" + )] + fn contents(contents: &str, snapshot: &str) { + let diagnostics = test_snippet( + contents, + &settings::Settings::for_rules(&Linter::Flake8TypeChecking), + ); + assert_messages!(snapshot, diagnostics); + } } diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index 616c542a953a5d..7a18a97afc9a5a 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -1,10 +1,14 @@ -use ruff_diagnostics::{Diagnostic, Violation}; +use rustpython_parser::ast::Stmt; + +use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_semantic::binding::{ Binding, BindingKind, FromImportation, Importation, SubmoduleImportation, }; +use crate::autofix; use crate::checkers::ast::Checker; +use crate::importer::StmtImport; use crate::registry::AsRule; use crate::rules::isort::{categorize, ImportSection, ImportType}; @@ -49,6 +53,8 @@ pub struct TypingOnlyFirstPartyImport { } impl Violation for TypingOnlyFirstPartyImport { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { format!( @@ -56,6 +62,10 @@ impl Violation for TypingOnlyFirstPartyImport { self.full_name ) } + + fn autofix_title(&self) -> Option { + Some("Move into type-checking block".to_string()) + } } /// ## What it does @@ -99,6 +109,8 @@ pub struct TypingOnlyThirdPartyImport { } impl Violation for TypingOnlyThirdPartyImport { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { format!( @@ -106,6 +118,10 @@ impl Violation for TypingOnlyThirdPartyImport { self.full_name ) } + + fn autofix_title(&self) -> Option { + Some("Move into type-checking block".to_string()) + } } /// ## What it does @@ -149,6 +165,8 @@ pub struct TypingOnlyStandardLibraryImport { } impl Violation for TypingOnlyStandardLibraryImport { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { format!( @@ -156,6 +174,10 @@ impl Violation for TypingOnlyStandardLibraryImport { self.full_name ) } + + fn autofix_title(&self) -> Option { + Some("Move into type-checking block".to_string()) + } } /// Return `true` if `this` is implicitly loaded via importing `that`. @@ -285,6 +307,10 @@ pub(crate) fn typing_only_runtime_import( return; } + let Some(reference_id) = binding.references.first() else { + return; + }; + if binding.context.is_runtime() && binding.is_used() && binding.references().all(|reference_id| { @@ -307,7 +333,7 @@ pub(crate) fn typing_only_runtime_import( .unwrap(); // Categorize the import. - let diagnostic = match categorize( + let mut diagnostic = match categorize( full_name, Some(level), &checker.settings.src, @@ -342,6 +368,42 @@ pub(crate) fn typing_only_runtime_import( } }; + if checker.patch(diagnostic.kind.rule()) { + diagnostic.try_set_fix(|| { + // Step 1) Remove the import. + let source = binding.source.unwrap(); + let deleted: Vec<&Stmt> = checker.deletions.iter().map(Into::into).collect(); + let stmt = checker.semantic_model().stmts[source]; + let parent = checker + .semantic_model() + .stmts + .parent_id(source) + .map(|id| checker.semantic_model().stmts[id]); + let remove_import_edit = autofix::edits::remove_unused_imports( + std::iter::once(full_name), + stmt, + parent, + &deleted, + checker.locator, + checker.indexer, + checker.stylist, + )?; + + // Step 2) Add the import to a `TYPE_CHECKING` block. + let reference = checker.semantic_model().references.resolve(*reference_id); + let (type_checking_edit, add_import_edit) = checker.importer.to_typing_import( + &StmtImport { stmt, full_name }, + reference.range().start(), + checker.semantic_model(), + )?; + + Ok( + Fix::suggested_edits(remove_import_edit, [type_checking_edit, add_import_edit]) + .sorted(), + ) + }); + } + if checker.enabled(diagnostic.kind.rule()) { diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__exempt_modules.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__exempt_modules.snap index 3e0bf5d7a0c3d1..4bdba29840a7e5 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__exempt_modules.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__exempt_modules.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -exempt_modules.py:14:12: TCH002 Move third-party import `flask` into a type-checking block +exempt_modules.py:14:12: TCH002 [*] Move third-party import `flask` into a type-checking block | 14 | def f(): 15 | import flask @@ -9,5 +9,22 @@ exempt_modules.py:14:12: TCH002 Move third-party import `flask` into a type-chec 16 | 17 | x: flask | + = help: Move into type-checking block + +ℹ Suggested fix + 1 |+from typing import TYPE_CHECKING + 2 |+ + 3 |+if TYPE_CHECKING: + 4 |+ import flask +1 5 | def f(): +2 6 | import pandas as pd +3 7 | +-------------------------------------------------------------------------------- +11 15 | +12 16 | +13 17 | def f(): +14 |- import flask +15 18 | +16 19 | x: flask diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__import_from.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__import_from.snap new file mode 100644 index 00000000000000..a1765eb8ebf94a --- /dev/null +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__import_from.snap @@ -0,0 +1,31 @@ +--- +source: crates/ruff/src/rules/flake8_type_checking/mod.rs +--- +:5:5: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block + | +5 | from pandas import ( +6 | DataFrame, # DataFrame + | ^^^^^^^^^ TCH002 +7 | Series, # Series +8 | ) + | + = help: Move into type-checking block + +ℹ Suggested fix +2 2 | from __future__ import annotations +3 3 | +4 4 | from pandas import ( +5 |- DataFrame, # DataFrame +6 5 | Series, # Series +7 6 | ) + 7 |+from typing import TYPE_CHECKING + 8 |+ + 9 |+if TYPE_CHECKING: + 10 |+ from pandas import ( + 11 |+ DataFrame, # DataFrame + 12 |+ ) +8 13 | +9 14 | def f(x: DataFrame): +10 15 | pass + + diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__import_from_type_checking_block.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__import_from_type_checking_block.snap new file mode 100644 index 00000000000000..d5771d58e79b8a --- /dev/null +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__import_from_type_checking_block.snap @@ -0,0 +1,30 @@ +--- +source: crates/ruff/src/rules/flake8_type_checking/mod.rs +--- +:7:5: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block + | + 7 | from pandas import ( + 8 | DataFrame, # DataFrame + | ^^^^^^^^^ TCH002 + 9 | Series, # Series +10 | ) + | + = help: Move into type-checking block + +ℹ Suggested fix +4 4 | from typing import TYPE_CHECKING +5 5 | +6 6 | from pandas import ( +7 |- DataFrame, # DataFrame +8 7 | Series, # Series +9 8 | ) +10 9 | +11 10 | if TYPE_CHECKING: + 11 |+ from pandas import ( + 12 |+ DataFrame, # DataFrame + 13 |+ ) +12 14 | import os +13 15 | +14 16 | def f(x: DataFrame): + + diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__no_typing_import.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__no_typing_import.snap new file mode 100644 index 00000000000000..06da4721e3e367 --- /dev/null +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__no_typing_import.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff/src/rules/flake8_type_checking/mod.rs +--- +:4:8: TCH002 [*] Move third-party import `pandas` into a type-checking block + | +4 | from __future__ import annotations +5 | +6 | import pandas as pd + | ^^^^^^^^^^^^ TCH002 +7 | +8 | def f(x: pd.DataFrame): + | + = help: Move into type-checking block + +ℹ Suggested fix +1 1 | +2 2 | from __future__ import annotations +3 3 | +4 |-import pandas as pd + 4 |+from typing import TYPE_CHECKING + 5 |+ + 6 |+if TYPE_CHECKING: + 7 |+ import pandas as pd +5 8 | +6 9 | def f(x: pd.DataFrame): +7 10 | pass + + diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap index e110f3c9c3859b..bee4fa7c529992 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -strict.py:27:21: TCH002 Move third-party import `pkg.A` into a type-checking block +strict.py:27:21: TCH002 [*] Move third-party import `pkg.A` into a type-checking block | 27 | # In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime. 28 | import pkg @@ -10,8 +10,27 @@ strict.py:27:21: TCH002 Move third-party import `pkg.A` into a type-checking blo 30 | 31 | def test(value: A): | + = help: Move into type-checking block -strict.py:35:21: TCH002 Move third-party import `pkg.A` into a type-checking block +ℹ Suggested fix +1 1 | from __future__ import annotations + 2 |+from typing import TYPE_CHECKING + 3 |+ + 4 |+if TYPE_CHECKING: + 5 |+ from pkg import A +2 6 | +3 7 | +4 8 | def f(): +-------------------------------------------------------------------------------- +24 28 | def f(): +25 29 | # In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime. +26 30 | import pkg +27 |- from pkg import A +28 31 | +29 32 | def test(value: A): +30 33 | return pkg.B() + +strict.py:35:21: TCH002 [*] Move third-party import `pkg.A` into a type-checking block | 35 | def f(): 36 | # In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime. @@ -20,8 +39,28 @@ strict.py:35:21: TCH002 Move third-party import `pkg.A` into a type-checking blo 38 | 39 | def test(value: A): | + = help: Move into type-checking block + +ℹ Suggested fix +1 1 | from __future__ import annotations + 2 |+from typing import TYPE_CHECKING + 3 |+ + 4 |+if TYPE_CHECKING: + 5 |+ from pkg import A +2 6 | +3 7 | +4 8 | def f(): +-------------------------------------------------------------------------------- +32 36 | +33 37 | def f(): +34 38 | # In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime. +35 |- from pkg import A, B + 39 |+ from pkg import B +36 40 | +37 41 | def test(value: A): +38 42 | return B() -strict.py:54:25: TCH002 Move third-party import `pkg.bar.A` into a type-checking block +strict.py:54:25: TCH002 [*] Move third-party import `pkg.bar.A` into a type-checking block | 54 | # In un-strict mode, this _should_ rase an error, since `pkg` is used at runtime. 55 | import pkg @@ -30,8 +69,27 @@ strict.py:54:25: TCH002 Move third-party import `pkg.bar.A` into a type-checking 57 | 58 | def test(value: A): | + = help: Move into type-checking block -strict.py:62:12: TCH002 Move third-party import `pkg` into a type-checking block +ℹ Suggested fix +1 1 | from __future__ import annotations + 2 |+from typing import TYPE_CHECKING + 3 |+ + 4 |+if TYPE_CHECKING: + 5 |+ from pkg.bar import A +2 6 | +3 7 | +4 8 | def f(): +-------------------------------------------------------------------------------- +51 55 | def f(): +52 56 | # In un-strict mode, this _should_ rase an error, since `pkg` is used at runtime. +53 57 | import pkg +54 |- from pkg.bar import A +55 58 | +56 59 | def test(value: A): +57 60 | return pkg.B() + +strict.py:62:12: TCH002 [*] Move third-party import `pkg` into a type-checking block | 62 | def f(): 63 | # In un-strict mode, this shouldn't rase an error, since `pkg.bar` is used at runtime. @@ -39,8 +97,27 @@ strict.py:62:12: TCH002 Move third-party import `pkg` into a type-checking block | ^^^ TCH002 65 | import pkg.bar as B | + = help: Move into type-checking block + +ℹ Suggested fix +1 1 | from __future__ import annotations + 2 |+from typing import TYPE_CHECKING + 3 |+ + 4 |+if TYPE_CHECKING: + 5 |+ import pkg +2 6 | +3 7 | +4 8 | def f(): +-------------------------------------------------------------------------------- +59 63 | +60 64 | def f(): +61 65 | # In un-strict mode, this shouldn't rase an error, since `pkg.bar` is used at runtime. +62 |- import pkg +63 66 | import pkg.bar as B +64 67 | +65 68 | def test(value: pkg.A): -strict.py:71:12: TCH002 Move third-party import `pkg.foo` into a type-checking block +strict.py:71:12: TCH002 [*] Move third-party import `pkg.foo` into a type-checking block | 71 | def f(): 72 | # In un-strict mode, this shouldn't rase an error, since `pkg.foo.bar` is used at runtime. @@ -48,8 +125,27 @@ strict.py:71:12: TCH002 Move third-party import `pkg.foo` into a type-checking b | ^^^^^^^^^^^^ TCH002 74 | import pkg.foo.bar as B | + = help: Move into type-checking block -strict.py:80:12: TCH002 Move third-party import `pkg` into a type-checking block +ℹ Suggested fix +1 1 | from __future__ import annotations + 2 |+from typing import TYPE_CHECKING + 3 |+ + 4 |+if TYPE_CHECKING: + 5 |+ import pkg.foo as F +2 6 | +3 7 | +4 8 | def f(): +-------------------------------------------------------------------------------- +68 72 | +69 73 | def f(): +70 74 | # In un-strict mode, this shouldn't rase an error, since `pkg.foo.bar` is used at runtime. +71 |- import pkg.foo as F +72 75 | import pkg.foo.bar as B +73 76 | +74 77 | def test(value: F.Foo): + +strict.py:80:12: TCH002 [*] Move third-party import `pkg` into a type-checking block | 80 | def f(): 81 | # In un-strict mode, this shouldn't rase an error, since `pkg.foo.bar` is used at runtime. @@ -57,8 +153,27 @@ strict.py:80:12: TCH002 Move third-party import `pkg` into a type-checking block | ^^^ TCH002 83 | import pkg.foo.bar as B | + = help: Move into type-checking block + +ℹ Suggested fix +1 1 | from __future__ import annotations + 2 |+from typing import TYPE_CHECKING + 3 |+ + 4 |+if TYPE_CHECKING: + 5 |+ import pkg +2 6 | +3 7 | +4 8 | def f(): +-------------------------------------------------------------------------------- +77 81 | +78 82 | def f(): +79 83 | # In un-strict mode, this shouldn't rase an error, since `pkg.foo.bar` is used at runtime. +80 |- import pkg +81 84 | import pkg.foo.bar as B +82 85 | +83 86 | def test(value: pkg.A): -strict.py:91:12: TCH002 Move third-party import `pkg` into a type-checking block +strict.py:91:12: TCH002 [*] Move third-party import `pkg` into a type-checking block | 91 | # Note that `pkg` is a prefix of `pkgfoo` which are both different modules. This is 92 | # testing the implementation. @@ -66,8 +181,27 @@ strict.py:91:12: TCH002 Move third-party import `pkg` into a type-checking block | ^^^ TCH002 94 | import pkgfoo.bar as B | + = help: Move into type-checking block -strict.py:101:12: TCH002 Move third-party import `pkg.foo` into a type-checking block +ℹ Suggested fix +1 1 | from __future__ import annotations + 2 |+from typing import TYPE_CHECKING + 3 |+ + 4 |+if TYPE_CHECKING: + 5 |+ import pkg +2 6 | +3 7 | +4 8 | def f(): +-------------------------------------------------------------------------------- +88 92 | # In un-strict mode, this _should_ rase an error, since `pkgfoo.bar` is used at runtime. +89 93 | # Note that `pkg` is a prefix of `pkgfoo` which are both different modules. This is +90 94 | # testing the implementation. +91 |- import pkg +92 95 | import pkgfoo.bar as B +93 96 | +94 97 | def test(value: pkg.A): + +strict.py:101:12: TCH002 [*] Move third-party import `pkg.foo` into a type-checking block | 101 | # In un-strict mode, this shouldn't raise an error, since `pkg.bar` is used at runtime. 102 | import pkg.bar as B @@ -76,5 +210,24 @@ strict.py:101:12: TCH002 Move third-party import `pkg.foo` into a type-checking 104 | 105 | def test(value: F.Foo): | + = help: Move into type-checking block + +ℹ Suggested fix +1 1 | from __future__ import annotations + 2 |+from typing import TYPE_CHECKING + 3 |+ + 4 |+if TYPE_CHECKING: + 5 |+ import pkg.foo as F +2 6 | +3 7 | +4 8 | def f(): +-------------------------------------------------------------------------------- +98 102 | def f(): +99 103 | # In un-strict mode, this shouldn't raise an error, since `pkg.bar` is used at runtime. +100 104 | import pkg.bar as B +101 |- import pkg.foo as F +102 105 | +103 106 | def test(value: F.Foo): +104 107 | return B.Bar() diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_after_usage.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_after_usage.snap new file mode 100644 index 00000000000000..a929edf675899d --- /dev/null +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_after_usage.snap @@ -0,0 +1,28 @@ +--- +source: crates/ruff/src/rules/flake8_type_checking/mod.rs +--- +:6:8: TCH002 [*] Move third-party import `pandas` into a type-checking block + | + 6 | from typing import TYPE_CHECKING + 7 | + 8 | import pandas as pd + | ^^^^^^^^^^^^ TCH002 + 9 | +10 | def f(x: pd.DataFrame): + | + = help: Move into type-checking block + +ℹ Suggested fix +3 3 | +4 4 | from typing import TYPE_CHECKING +5 5 | +6 |-import pandas as pd +7 6 | + 7 |+if TYPE_CHECKING: + 8 |+ import pandas as pd + 9 |+ +8 10 | def f(x: pd.DataFrame): +9 11 | pass +10 12 | + + diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_inline.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_inline.snap new file mode 100644 index 00000000000000..28788c18e52963 --- /dev/null +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_inline.snap @@ -0,0 +1,27 @@ +--- +source: crates/ruff/src/rules/flake8_type_checking/mod.rs +--- +:6:8: TCH002 [*] Move third-party import `pandas` into a type-checking block + | + 6 | from typing import TYPE_CHECKING + 7 | + 8 | import pandas as pd + | ^^^^^^^^^^^^ TCH002 + 9 | +10 | if TYPE_CHECKING: import os + | + = help: Move into type-checking block + +ℹ Suggested fix +3 3 | +4 4 | from typing import TYPE_CHECKING +5 5 | +6 |-import pandas as pd +7 6 | +8 |-if TYPE_CHECKING: import os + 7 |+if TYPE_CHECKING: import pandas as pd; import os +9 8 | +10 9 | def f(x: pd.DataFrame): +11 10 | pass + + diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_own_line.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_own_line.snap new file mode 100644 index 00000000000000..28ca5ee6eeae80 --- /dev/null +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__type_checking_block_own_line.snap @@ -0,0 +1,27 @@ +--- +source: crates/ruff/src/rules/flake8_type_checking/mod.rs +--- +:6:8: TCH002 [*] Move third-party import `pandas` into a type-checking block + | + 6 | from typing import TYPE_CHECKING + 7 | + 8 | import pandas as pd + | ^^^^^^^^^^^^ TCH002 + 9 | +10 | if TYPE_CHECKING: + | + = help: Move into type-checking block + +ℹ Suggested fix +3 3 | +4 4 | from typing import TYPE_CHECKING +5 5 | +6 |-import pandas as pd +7 6 | +8 7 | if TYPE_CHECKING: + 8 |+ import pandas as pd +9 9 | import os +10 10 | +11 11 | def f(x: pd.DataFrame): + + diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-first-party-import_TCH001.py.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-first-party-import_TCH001.py.snap index de908746441a26..ac302297dccd9f 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-first-party-import_TCH001.py.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-first-party-import_TCH001.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -TCH001.py:20:19: TCH001 Move application import `.TYP001` into a type-checking block +TCH001.py:20:19: TCH001 [*] Move application import `.TYP001` into a type-checking block | 20 | def f(): 21 | from . import TYP001 @@ -9,5 +9,26 @@ TCH001.py:20:19: TCH001 Move application import `.TYP001` into a type-checking b 22 | 23 | x: TYP001 | + = help: Move into type-checking block + +ℹ Suggested fix +2 2 | +3 3 | For typing-only import detection tests, see `TCH002.py`. +4 4 | """ + 5 |+from typing import TYPE_CHECKING + 6 |+ + 7 |+if TYPE_CHECKING: + 8 |+ from . import TYP001 +5 9 | +6 10 | +7 11 | def f(): +-------------------------------------------------------------------------------- +17 21 | +18 22 | +19 23 | def f(): +20 |- from . import TYP001 +21 24 | +22 25 | x: TYP001 +23 26 | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-standard-library-import_TCH003.py.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-standard-library-import_TCH003.py.snap index 2c3b8bcb488f0e..5e850bff867eb6 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-standard-library-import_TCH003.py.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-standard-library-import_TCH003.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -TCH003.py:8:12: TCH003 Move standard library import `os` into a type-checking block +TCH003.py:8:12: TCH003 [*] Move standard library import `os` into a type-checking block | 8 | def f(): 9 | import os @@ -9,5 +9,22 @@ TCH003.py:8:12: TCH003 Move standard library import `os` into a type-checking bl 10 | 11 | x: os | + = help: Move into type-checking block + +ℹ Suggested fix +2 2 | +3 3 | For typing-only import detection tests, see `TCH002.py`. +4 4 | """ + 5 |+from typing import TYPE_CHECKING + 6 |+ + 7 |+if TYPE_CHECKING: + 8 |+ import os +5 9 | +6 10 | +7 11 | def f(): +8 |- import os +9 12 | +10 13 | x: os +11 14 | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-standard-library-import_runtime_evaluated_base_classes_3.py.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-standard-library-import_runtime_evaluated_base_classes_3.py.snap index 6cc9df1b97f02c..cb75d42cbf7e51 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-standard-library-import_runtime_evaluated_base_classes_3.py.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-standard-library-import_runtime_evaluated_base_classes_3.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -runtime_evaluated_base_classes_3.py:5:18: TCH003 Move standard library import `uuid.UUID` into a type-checking block +runtime_evaluated_base_classes_3.py:5:18: TCH003 [*] Move standard library import `uuid.UUID` into a type-checking block | 5 | import datetime 6 | import pathlib @@ -10,5 +10,22 @@ runtime_evaluated_base_classes_3.py:5:18: TCH003 Move standard library import `u 8 | 9 | import pydantic | + = help: Move into type-checking block + +ℹ Suggested fix +2 2 | +3 3 | import datetime +4 4 | import pathlib +5 |-from uuid import UUID # TCH003 +6 5 | +7 6 | import pydantic +8 7 | from pydantic import BaseModel + 8 |+from typing import TYPE_CHECKING + 9 |+ + 10 |+if TYPE_CHECKING: + 11 |+ from uuid import UUID +9 12 | +10 13 | +11 14 | class A(pydantic.BaseModel): diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-standard-library-import_runtime_evaluated_decorators_3.py.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-standard-library-import_runtime_evaluated_decorators_3.py.snap index 865cacc0379f96..46574c3418ed34 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-standard-library-import_runtime_evaluated_decorators_3.py.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-standard-library-import_runtime_evaluated_decorators_3.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -runtime_evaluated_decorators_3.py:6:18: TCH003 Move standard library import `uuid.UUID` into a type-checking block +runtime_evaluated_decorators_3.py:6:18: TCH003 [*] Move standard library import `uuid.UUID` into a type-checking block | 6 | from array import array 7 | from dataclasses import dataclass @@ -10,5 +10,22 @@ runtime_evaluated_decorators_3.py:6:18: TCH003 Move standard library import `uui 9 | 10 | import attrs | + = help: Move into type-checking block + +ℹ Suggested fix +3 3 | import datetime +4 4 | from array import array +5 5 | from dataclasses import dataclass +6 |-from uuid import UUID # TCH003 +7 6 | +8 7 | import attrs +9 8 | from attrs import frozen + 9 |+from typing import TYPE_CHECKING + 10 |+ + 11 |+if TYPE_CHECKING: + 12 |+ from uuid import UUID +10 13 | +11 14 | +12 15 | @attrs.define(auto_attribs=True) diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap index 3d2701eec4ac47..26ce364ee1e04b 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_TCH002.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -TCH002.py:5:12: TCH002 Move third-party import `pandas` into a type-checking block +TCH002.py:5:12: TCH002 [*] Move third-party import `pandas` into a type-checking block | 5 | def f(): 6 | import pandas as pd # TCH002 @@ -9,8 +9,23 @@ TCH002.py:5:12: TCH002 Move third-party import `pandas` into a type-checking blo 7 | 8 | x: pd.DataFrame | + = help: Move into type-checking block -TCH002.py:11:24: TCH002 Move third-party import `pandas.DataFrame` into a type-checking block +ℹ Suggested fix +1 1 | """Tests to determine accurate detection of typing-only imports.""" + 2 |+from typing import TYPE_CHECKING + 3 |+ + 4 |+if TYPE_CHECKING: + 5 |+ import pandas as pd +2 6 | +3 7 | +4 8 | def f(): +5 |- import pandas as pd # TCH002 +6 9 | +7 10 | x: pd.DataFrame +8 11 | + +TCH002.py:11:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block | 11 | def f(): 12 | from pandas import DataFrame # TCH002 @@ -18,8 +33,27 @@ TCH002.py:11:24: TCH002 Move third-party import `pandas.DataFrame` into a type-c 13 | 14 | x: DataFrame | + = help: Move into type-checking block + +ℹ Suggested fix +1 1 | """Tests to determine accurate detection of typing-only imports.""" + 2 |+from typing import TYPE_CHECKING + 3 |+ + 4 |+if TYPE_CHECKING: + 5 |+ from pandas import DataFrame +2 6 | +3 7 | +4 8 | def f(): +-------------------------------------------------------------------------------- +8 12 | +9 13 | +10 14 | def f(): +11 |- from pandas import DataFrame # TCH002 +12 15 | +13 16 | x: DataFrame +14 17 | -TCH002.py:17:24: TCH002 Move third-party import `pandas.DataFrame` into a type-checking block +TCH002.py:17:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block | 17 | def f(): 18 | from pandas import DataFrame as df # TCH002 @@ -27,8 +61,27 @@ TCH002.py:17:24: TCH002 Move third-party import `pandas.DataFrame` into a type-c 19 | 20 | x: df | + = help: Move into type-checking block -TCH002.py:23:12: TCH002 Move third-party import `pandas` into a type-checking block +ℹ Suggested fix +1 1 | """Tests to determine accurate detection of typing-only imports.""" + 2 |+from typing import TYPE_CHECKING + 3 |+ + 4 |+if TYPE_CHECKING: + 5 |+ from pandas import DataFrame as df +2 6 | +3 7 | +4 8 | def f(): +-------------------------------------------------------------------------------- +14 18 | +15 19 | +16 20 | def f(): +17 |- from pandas import DataFrame as df # TCH002 +18 21 | +19 22 | x: df +20 23 | + +TCH002.py:23:12: TCH002 [*] Move third-party import `pandas` into a type-checking block | 23 | def f(): 24 | import pandas as pd # TCH002 @@ -36,8 +89,27 @@ TCH002.py:23:12: TCH002 Move third-party import `pandas` into a type-checking bl 25 | 26 | x: pd.DataFrame = 1 | + = help: Move into type-checking block + +ℹ Suggested fix +1 1 | """Tests to determine accurate detection of typing-only imports.""" + 2 |+from typing import TYPE_CHECKING + 3 |+ + 4 |+if TYPE_CHECKING: + 5 |+ import pandas as pd +2 6 | +3 7 | +4 8 | def f(): +-------------------------------------------------------------------------------- +20 24 | +21 25 | +22 26 | def f(): +23 |- import pandas as pd # TCH002 +24 27 | +25 28 | x: pd.DataFrame = 1 +26 29 | -TCH002.py:29:24: TCH002 Move third-party import `pandas.DataFrame` into a type-checking block +TCH002.py:29:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block | 29 | def f(): 30 | from pandas import DataFrame # TCH002 @@ -45,8 +117,27 @@ TCH002.py:29:24: TCH002 Move third-party import `pandas.DataFrame` into a type-c 31 | 32 | x: DataFrame = 2 | + = help: Move into type-checking block -TCH002.py:35:24: TCH002 Move third-party import `pandas.DataFrame` into a type-checking block +ℹ Suggested fix +1 1 | """Tests to determine accurate detection of typing-only imports.""" + 2 |+from typing import TYPE_CHECKING + 3 |+ + 4 |+if TYPE_CHECKING: + 5 |+ from pandas import DataFrame +2 6 | +3 7 | +4 8 | def f(): +-------------------------------------------------------------------------------- +26 30 | +27 31 | +28 32 | def f(): +29 |- from pandas import DataFrame # TCH002 +30 33 | +31 34 | x: DataFrame = 2 +32 35 | + +TCH002.py:35:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block | 35 | def f(): 36 | from pandas import DataFrame as df # TCH002 @@ -54,8 +145,27 @@ TCH002.py:35:24: TCH002 Move third-party import `pandas.DataFrame` into a type-c 37 | 38 | x: df = 3 | + = help: Move into type-checking block + +ℹ Suggested fix +1 1 | """Tests to determine accurate detection of typing-only imports.""" + 2 |+from typing import TYPE_CHECKING + 3 |+ + 4 |+if TYPE_CHECKING: + 5 |+ from pandas import DataFrame as df +2 6 | +3 7 | +4 8 | def f(): +-------------------------------------------------------------------------------- +32 36 | +33 37 | +34 38 | def f(): +35 |- from pandas import DataFrame as df # TCH002 +36 39 | +37 40 | x: df = 3 +38 41 | -TCH002.py:41:12: TCH002 Move third-party import `pandas` into a type-checking block +TCH002.py:41:12: TCH002 [*] Move third-party import `pandas` into a type-checking block | 41 | def f(): 42 | import pandas as pd # TCH002 @@ -63,8 +173,27 @@ TCH002.py:41:12: TCH002 Move third-party import `pandas` into a type-checking bl 43 | 44 | x: "pd.DataFrame" = 1 | + = help: Move into type-checking block -TCH002.py:47:12: TCH002 Move third-party import `pandas` into a type-checking block +ℹ Suggested fix +1 1 | """Tests to determine accurate detection of typing-only imports.""" + 2 |+from typing import TYPE_CHECKING + 3 |+ + 4 |+if TYPE_CHECKING: + 5 |+ import pandas as pd +2 6 | +3 7 | +4 8 | def f(): +-------------------------------------------------------------------------------- +38 42 | +39 43 | +40 44 | def f(): +41 |- import pandas as pd # TCH002 +42 45 | +43 46 | x: "pd.DataFrame" = 1 +44 47 | + +TCH002.py:47:12: TCH002 [*] Move third-party import `pandas` into a type-checking block | 47 | def f(): 48 | import pandas as pd # TCH002 @@ -72,5 +201,24 @@ TCH002.py:47:12: TCH002 Move third-party import `pandas` into a type-checking bl 49 | 50 | x = dict["pd.DataFrame", "pd.DataFrame"] | + = help: Move into type-checking block + +ℹ Suggested fix +1 1 | """Tests to determine accurate detection of typing-only imports.""" + 2 |+from typing import TYPE_CHECKING + 3 |+ + 4 |+if TYPE_CHECKING: + 5 |+ import pandas as pd +2 6 | +3 7 | +4 8 | def f(): +-------------------------------------------------------------------------------- +44 48 | +45 49 | +46 50 | def f(): +47 |- import pandas as pd # TCH002 +48 51 | +49 52 | x = dict["pd.DataFrame", "pd.DataFrame"] +50 53 | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_base_classes_2.py.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_base_classes_2.py.snap index 247674c3fef514..9c1052549cf8e8 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_base_classes_2.py.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_base_classes_2.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -runtime_evaluated_base_classes_2.py:3:8: TCH002 Move third-party import `geopandas` into a type-checking block +runtime_evaluated_base_classes_2.py:3:8: TCH002 [*] Move third-party import `geopandas` into a type-checking block | 3 | from __future__ import annotations 4 | @@ -10,8 +10,26 @@ runtime_evaluated_base_classes_2.py:3:8: TCH002 Move third-party import `geopand 6 | import pydantic 7 | import pyproj # TCH002 | + = help: Move into type-checking block -runtime_evaluated_base_classes_2.py:5:8: TCH002 Move third-party import `pyproj` into a type-checking block +ℹ Suggested fix +1 1 | from __future__ import annotations +2 2 | +3 |-import geopandas as gpd # TCH002 +4 3 | import pydantic +5 4 | import pyproj # TCH002 +6 5 | from pydantic import BaseModel +7 6 | +8 7 | import numpy + 8 |+from typing import TYPE_CHECKING + 9 |+ + 10 |+if TYPE_CHECKING: + 11 |+ import geopandas as gpd +9 12 | +10 13 | +11 14 | class A(BaseModel): + +runtime_evaluated_base_classes_2.py:5:8: TCH002 [*] Move third-party import `pyproj` into a type-checking block | 5 | import geopandas as gpd # TCH002 6 | import pydantic @@ -19,5 +37,22 @@ runtime_evaluated_base_classes_2.py:5:8: TCH002 Move third-party import `pyproj` | ^^^^^^ TCH002 8 | from pydantic import BaseModel | + = help: Move into type-checking block + +ℹ Suggested fix +2 2 | +3 3 | import geopandas as gpd # TCH002 +4 4 | import pydantic +5 |-import pyproj # TCH002 +6 5 | from pydantic import BaseModel +7 6 | +8 7 | import numpy + 8 |+from typing import TYPE_CHECKING + 9 |+ + 10 |+if TYPE_CHECKING: + 11 |+ import pyproj +9 12 | +10 13 | +11 14 | class A(BaseModel): diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_decorators_2.py.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_decorators_2.py.snap index 00cf182eaf7a73..c5f29c943de403 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_decorators_2.py.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_runtime_evaluated_decorators_2.py.snap @@ -1,12 +1,26 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -runtime_evaluated_decorators_2.py:10:8: TCH002 Move third-party import `numpy` into a type-checking block +runtime_evaluated_decorators_2.py:10:8: TCH002 [*] Move third-party import `numpy` into a type-checking block | 10 | from attrs import frozen 11 | 12 | import numpy # TCH002 | ^^^^^ TCH002 | + = help: Move into type-checking block + +ℹ Suggested fix +7 7 | import pyproj +8 8 | from attrs import frozen +9 9 | +10 |-import numpy # TCH002 + 10 |+from typing import TYPE_CHECKING + 11 |+ + 12 |+if TYPE_CHECKING: + 13 |+ import numpy +11 14 | +12 15 | +13 16 | @attrs.define(auto_attribs=True) diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_strict.py.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_strict.py.snap index b3044d4b63d0ae..9d6efbff990701 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_strict.py.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_strict.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- -strict.py:54:25: TCH002 Move third-party import `pkg.bar.A` into a type-checking block +strict.py:54:25: TCH002 [*] Move third-party import `pkg.bar.A` into a type-checking block | 54 | # In un-strict mode, this _should_ rase an error, since `pkg` is used at runtime. 55 | import pkg @@ -10,8 +10,27 @@ strict.py:54:25: TCH002 Move third-party import `pkg.bar.A` into a type-checking 57 | 58 | def test(value: A): | + = help: Move into type-checking block -strict.py:91:12: TCH002 Move third-party import `pkg` into a type-checking block +ℹ Suggested fix +1 1 | from __future__ import annotations + 2 |+from typing import TYPE_CHECKING + 3 |+ + 4 |+if TYPE_CHECKING: + 5 |+ from pkg.bar import A +2 6 | +3 7 | +4 8 | def f(): +-------------------------------------------------------------------------------- +51 55 | def f(): +52 56 | # In un-strict mode, this _should_ rase an error, since `pkg` is used at runtime. +53 57 | import pkg +54 |- from pkg.bar import A +55 58 | +56 59 | def test(value: A): +57 60 | return pkg.B() + +strict.py:91:12: TCH002 [*] Move third-party import `pkg` into a type-checking block | 91 | # Note that `pkg` is a prefix of `pkgfoo` which are both different modules. This is 92 | # testing the implementation. @@ -19,5 +38,24 @@ strict.py:91:12: TCH002 Move third-party import `pkg` into a type-checking block | ^^^ TCH002 94 | import pkgfoo.bar as B | + = help: Move into type-checking block + +ℹ Suggested fix +1 1 | from __future__ import annotations + 2 |+from typing import TYPE_CHECKING + 3 |+ + 4 |+if TYPE_CHECKING: + 5 |+ import pkg +2 6 | +3 7 | +4 8 | def f(): +-------------------------------------------------------------------------------- +88 92 | # In un-strict mode, this _should_ rase an error, since `pkgfoo.bar` is used at runtime. +89 93 | # Note that `pkg` is a prefix of `pkgfoo` which are both different modules. This is +90 94 | # testing the implementation. +91 |- import pkg +92 95 | import pkgfoo.bar as B +93 96 | +94 97 | def test(value: pkg.A): diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_package_import.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_package_import.snap new file mode 100644 index 00000000000000..8c430594c2855c --- /dev/null +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_package_import.snap @@ -0,0 +1,29 @@ +--- +source: crates/ruff/src/rules/flake8_type_checking/mod.rs +--- +:4:8: TCH002 [*] Move third-party import `pandas` into a type-checking block + | +4 | from __future__ import annotations +5 | +6 | import pandas as pd + | ^^^^^^^^^^^^ TCH002 +7 | +8 | from typing import TYPE_CHECKING + | + = help: Move into type-checking block + +ℹ Suggested fix +1 1 | +2 2 | from __future__ import annotations +3 3 | +4 |-import pandas as pd +5 4 | +6 5 | from typing import TYPE_CHECKING +7 6 | + 7 |+if TYPE_CHECKING: + 8 |+ import pandas as pd + 9 |+ +8 10 | def f(x: pd.DataFrame): +9 11 | pass + + diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_usage.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_usage.snap new file mode 100644 index 00000000000000..683b75f97929c3 --- /dev/null +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_after_usage.snap @@ -0,0 +1,15 @@ +--- +source: crates/ruff/src/rules/flake8_type_checking/mod.rs +--- +:4:8: TCH002 Move third-party import `pandas` into a type-checking block + | +4 | from __future__ import annotations +5 | +6 | import pandas as pd + | ^^^^^^^^^^^^ TCH002 +7 | +8 | def f(x: pd.DataFrame): + | + = help: Move into type-checking block + + diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_before_package_import.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_before_package_import.snap new file mode 100644 index 00000000000000..edc33e0366a6bc --- /dev/null +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing_import_before_package_import.snap @@ -0,0 +1,27 @@ +--- +source: crates/ruff/src/rules/flake8_type_checking/mod.rs +--- +:6:8: TCH002 [*] Move third-party import `pandas` into a type-checking block + | + 6 | from typing import TYPE_CHECKING + 7 | + 8 | import pandas as pd + | ^^^^^^^^^^^^ TCH002 + 9 | +10 | def f(x: pd.DataFrame): + | + = help: Move into type-checking block + +ℹ Suggested fix +3 3 | +4 4 | from typing import TYPE_CHECKING +5 5 | +6 |-import pandas as pd +7 6 | + 7 |+if TYPE_CHECKING: + 8 |+ import pandas as pd + 9 |+ +8 10 | def f(x: pd.DataFrame): +9 11 | pass + + diff --git a/crates/ruff_diagnostics/src/fix.rs b/crates/ruff_diagnostics/src/fix.rs index 2769fca88c6430..da2b034ded197e 100644 --- a/crates/ruff_diagnostics/src/fix.rs +++ b/crates/ruff_diagnostics/src/fix.rs @@ -123,4 +123,11 @@ impl Fix { pub fn applicability(&self) -> Applicability { self.applicability } + + /// Sort the [`Edit`] elements in the [`Fix`] by their [`Edit::start`] position. + #[must_use] + pub fn sorted(mut self) -> Self { + self.edits.sort_by_key(Edit::start); + self + } }