Add Checker::import_from_typing (#17340)

Summary
--

This PR replaces uses of version-dependent imports from `typing` or
`typing_extensions` with a centralized `Checker::import_from_typing`
method.

The idea here is to make the fix for #9761 (whatever it ends up being)
applicable to all of the rules performing similar checks.

Test Plan
--

Existing tests for the affected rules.
This commit is contained in:
Brent Westbrook 2025-04-11 09:37:55 -04:00 committed by GitHub
parent 1aad180aae
commit 8e11c53310
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 87 additions and 140 deletions

View file

@ -31,7 +31,7 @@ use ruff_python_parser::semantic_errors::{
}; };
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
use ruff_diagnostics::{Diagnostic, IsolationLevel}; use ruff_diagnostics::{Diagnostic, Edit, IsolationLevel};
use ruff_notebook::{CellOffsets, NotebookIndex}; use ruff_notebook::{CellOffsets, NotebookIndex};
use ruff_python_ast::helpers::{collect_import_from_member, is_docstring_stmt, to_module_path}; use ruff_python_ast::helpers::{collect_import_from_member, is_docstring_stmt, to_module_path};
use ruff_python_ast::identifier::Identifier; use ruff_python_ast::identifier::Identifier;
@ -62,7 +62,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::annotation::AnnotationContext; use crate::checkers::ast::annotation::AnnotationContext;
use crate::docstrings::extraction::ExtractionTarget; use crate::docstrings::extraction::ExtractionTarget;
use crate::importer::Importer; use crate::importer::{ImportRequest, Importer, ResolutionError};
use crate::noqa::NoqaMapping; use crate::noqa::NoqaMapping;
use crate::package::PackageRoot; use crate::package::PackageRoot;
use crate::registry::Rule; use crate::registry::Rule;
@ -530,6 +530,28 @@ impl<'a> Checker<'a> {
f(&mut checker, self); f(&mut checker, self);
self.semantic_checker = checker; self.semantic_checker = checker;
} }
/// Attempt to create an [`Edit`] that imports `member`.
///
/// On Python <`version_added_to_typing`, `member` is imported from `typing_extensions`, while
/// on Python >=`version_added_to_typing`, it is imported from `typing`.
///
/// See [`Importer::get_or_import_symbol`] for more details on the returned values.
pub(crate) fn import_from_typing(
&self,
member: &str,
position: TextSize,
version_added_to_typing: PythonVersion,
) -> Result<(Edit, String), ResolutionError> {
let source_module = if self.target_version() >= version_added_to_typing {
"typing"
} else {
"typing_extensions"
};
let request = ImportRequest::import_from(source_module, member);
self.importer()
.get_or_import_symbol(&request, position, self.semantic())
}
} }
impl SemanticSyntaxContext for Checker<'_> { impl SemanticSyntaxContext for Checker<'_> {

View file

@ -6,7 +6,6 @@ use ruff_python_semantic::Modules;
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::rules::fastapi::rules::is_fastapi_route; use crate::rules::fastapi::rules::is_fastapi_route;
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;
@ -232,15 +231,10 @@ fn create_diagnostic(
); );
let try_generate_fix = || { let try_generate_fix = || {
let module = if checker.target_version() >= PythonVersion::PY39 { let (import_edit, binding) = checker.import_from_typing(
"typing" "Annotated",
} else {
"typing_extensions"
};
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import_from(module, "Annotated"),
parameter.range.start(), parameter.range.start(),
checker.semantic(), PythonVersion::PY39,
)?; )?;
// Each of these classes takes a single, optional default // Each of these classes takes a single, optional default

View file

@ -14,7 +14,7 @@ use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Definition, SemanticModel}; use ruff_python_semantic::{Definition, SemanticModel};
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
use crate::importer::{ImportRequest, Importer}; use crate::checkers::ast::Checker;
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;
/// Return the name of the function, if it's overloaded. /// Return the name of the function, if it's overloaded.
@ -119,26 +119,19 @@ impl AutoPythonType {
/// additional edits. /// additional edits.
pub(crate) fn into_expression( pub(crate) fn into_expression(
self, self,
importer: &Importer, checker: &Checker,
at: TextSize, at: TextSize,
semantic: &SemanticModel,
target_version: PythonVersion,
) -> Option<(Expr, Vec<Edit>)> { ) -> Option<(Expr, Vec<Edit>)> {
let target_version = checker.target_version();
match self { match self {
AutoPythonType::Never => { AutoPythonType::Never => {
let (no_return_edit, binding) = importer let member = if target_version >= PythonVersion::PY311 {
.get_or_import_symbol( "Never"
&ImportRequest::import_from( } else {
"typing", "NoReturn"
if target_version >= PythonVersion::PY311 { };
"Never" let (no_return_edit, binding) = checker
} else { .import_from_typing(member, at, PythonVersion::lowest())
"NoReturn"
},
),
at,
semantic,
)
.ok()?; .ok()?;
let expr = Expr::Name(ast::ExprName { let expr = Expr::Name(ast::ExprName {
id: Name::from(binding), id: Name::from(binding),
@ -175,12 +168,8 @@ impl AutoPythonType {
let element = type_expr(*python_type)?; let element = type_expr(*python_type)?;
// Ex) `Optional[int]` // Ex) `Optional[int]`
let (optional_edit, binding) = importer let (optional_edit, binding) = checker
.get_or_import_symbol( .import_from_typing("Optional", at, PythonVersion::lowest())
&ImportRequest::import_from("typing", "Optional"),
at,
semantic,
)
.ok()?; .ok()?;
let expr = typing_optional(element, Name::from(binding)); let expr = typing_optional(element, Name::from(binding));
Some((expr, vec![optional_edit])) Some((expr, vec![optional_edit]))
@ -192,12 +181,8 @@ impl AutoPythonType {
.collect::<Option<Vec<_>>>()?; .collect::<Option<Vec<_>>>()?;
// Ex) `Union[int, str]` // Ex) `Union[int, str]`
let (union_edit, binding) = importer let (union_edit, binding) = checker
.get_or_import_symbol( .import_from_typing("Union", at, PythonVersion::lowest())
&ImportRequest::import_from("typing", "Union"),
at,
semantic,
)
.ok()?; .ok()?;
let expr = typing_union(&elements, Name::from(binding)); let expr = typing_union(&elements, Name::from(binding));
Some((expr, vec![union_edit])) Some((expr, vec![union_edit]))

View file

@ -721,12 +721,7 @@ pub(crate) fn definition(
} else { } else {
auto_return_type(function) auto_return_type(function)
.and_then(|return_type| { .and_then(|return_type| {
return_type.into_expression( return_type.into_expression(checker, function.parameters.start())
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.target_version(),
)
}) })
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits)) .map(|(return_type, edits)| (checker.generator().expr(&return_type), edits))
}; };
@ -752,12 +747,7 @@ pub(crate) fn definition(
} else { } else {
auto_return_type(function) auto_return_type(function)
.and_then(|return_type| { .and_then(|return_type| {
return_type.into_expression( return_type.into_expression(checker, function.parameters.start())
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.target_version(),
)
}) })
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits)) .map(|(return_type, edits)| (checker.generator().expr(&return_type), edits))
}; };
@ -822,12 +812,8 @@ pub(crate) fn definition(
} else { } else {
auto_return_type(function) auto_return_type(function)
.and_then(|return_type| { .and_then(|return_type| {
return_type.into_expression( return_type
checker.importer(), .into_expression(checker, function.parameters.start())
function.parameters.start(),
checker.semantic(),
checker.target_version(),
)
}) })
.map(|(return_type, edits)| { .map(|(return_type, edits)| {
(checker.generator().expr(&return_type), edits) (checker.generator().expr(&return_type), edits)
@ -861,12 +847,8 @@ pub(crate) fn definition(
} else { } else {
auto_return_type(function) auto_return_type(function)
.and_then(|return_type| { .and_then(|return_type| {
return_type.into_expression( return_type
checker.importer(), .into_expression(checker, function.parameters.start())
function.parameters.start(),
checker.semantic(),
checker.target_version(),
)
}) })
.map(|(return_type, edits)| { .map(|(return_type, edits)| {
(checker.generator().expr(&return_type), edits) (checker.generator().expr(&return_type), edits)

View file

@ -8,10 +8,9 @@ use ruff_python_semantic::analyze::class::is_metaclass;
use ruff_python_semantic::analyze::function_type::{self, FunctionType}; use ruff_python_semantic::analyze::function_type::{self, FunctionType};
use ruff_python_semantic::analyze::visibility::{is_abstract, is_overload}; use ruff_python_semantic::analyze::visibility::{is_abstract, is_overload};
use ruff_python_semantic::{Binding, ResolvedReference, ScopeId, SemanticModel}; use ruff_python_semantic::{Binding, ResolvedReference, ScopeId, SemanticModel};
use ruff_text_size::{Ranged, TextRange, TextSize}; use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::importer::{ImportRequest, ResolutionError};
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;
/// ## What it does /// ## What it does
@ -317,7 +316,8 @@ fn replace_custom_typevar_with_self(
self_or_cls_annotation: &ast::Expr, self_or_cls_annotation: &ast::Expr,
) -> anyhow::Result<Fix> { ) -> anyhow::Result<Fix> {
// (1) Import `Self` (if necessary) // (1) Import `Self` (if necessary)
let (import_edit, self_symbol_binding) = import_self(checker, function_def.start())?; let (import_edit, self_symbol_binding) =
checker.import_from_typing("Self", function_def.start(), PythonVersion::PY311)?;
// (2) Remove the first parameter's annotation // (2) Remove the first parameter's annotation
let mut other_edits = vec![Edit::deletion( let mut other_edits = vec![Edit::deletion(
@ -367,24 +367,6 @@ fn replace_custom_typevar_with_self(
)) ))
} }
/// Attempt to create an [`Edit`] that imports `Self`.
///
/// On Python <3.11, `Self` is imported from `typing_extensions`;
/// on Python >=3.11, it is imported from `typing`.
/// This is because it was added to the `typing` module on Python 3.11,
/// but is available from the backport package `typing_extensions` on all versions.
fn import_self(checker: &Checker, position: TextSize) -> Result<(Edit, String), ResolutionError> {
let source_module = if checker.target_version() >= PythonVersion::PY311 {
"typing"
} else {
"typing_extensions"
};
let request = ImportRequest::import_from(source_module, "Self");
checker
.importer()
.get_or_import_symbol(&request, position, checker.semantic())
}
/// Returns a series of [`Edit`]s that modify all references to the given `typevar`. /// Returns a series of [`Edit`]s that modify all references to the given `typevar`.
/// ///
/// Only references within `editable_range` will be modified. /// Only references within `editable_range` will be modified.

View file

@ -2,18 +2,19 @@ use std::collections::HashSet;
use anyhow::Result; use anyhow::Result;
use ruff_python_ast::name::Name;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::comparable::ComparableExpr; use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::{Expr, ExprBinOp, ExprContext, ExprName, ExprSubscript, ExprTuple, Operator}; use ruff_python_ast::name::Name;
use ruff_python_ast::{
Expr, ExprBinOp, ExprContext, ExprName, ExprSubscript, ExprTuple, Operator, PythonVersion,
};
use ruff_python_semantic::analyze::typing::traverse_union; use ruff_python_semantic::analyze::typing::traverse_union;
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
/// ## What it does /// ## What it does
/// Checks for duplicate union members. /// Checks for duplicate union members.
@ -181,11 +182,8 @@ fn generate_union_fix(
debug_assert!(nodes.len() >= 2, "At least two nodes required"); debug_assert!(nodes.len() >= 2, "At least two nodes required");
// Request `typing.Union` // Request `typing.Union`
let (import_edit, binding) = checker.importer().get_or_import_symbol( let (import_edit, binding) =
&ImportRequest::import_from("typing", "Union"), checker.import_from_typing("Union", annotation.start(), PythonVersion::lowest())?;
annotation.start(),
checker.semantic(),
)?;
// Construct the expression as `Subscript[typing.Union, Tuple[expr, [expr, ...]]]` // Construct the expression as `Subscript[typing.Union, Tuple[expr, [expr, ...]]]`
let new_expr = Expr::Subscript(ExprSubscript { let new_expr = Expr::Subscript(ExprSubscript {

View file

@ -1,5 +1,4 @@
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast as ast; use ruff_python_ast as ast;
@ -214,17 +213,8 @@ fn replace_with_self_fix(
) -> anyhow::Result<Fix> { ) -> anyhow::Result<Fix> {
let semantic = checker.semantic(); let semantic = checker.semantic();
let (self_import, self_binding) = { let (self_import, self_binding) =
let source_module = if checker.target_version() >= PythonVersion::PY311 { checker.import_from_typing("Self", returns.start(), PythonVersion::PY311)?;
"typing"
} else {
"typing_extensions"
};
let (importer, semantic) = (checker.importer(), checker.semantic());
let request = ImportRequest::import_from(source_module, "Self");
importer.get_or_import_symbol(&request, returns.start(), semantic)?
};
let mut others = Vec::with_capacity(2); let mut others = Vec::with_capacity(2);

View file

@ -12,7 +12,7 @@ use ruff_text_size::{Ranged, TextRange};
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::{checkers::ast::Checker, importer::ImportRequest}; use crate::checkers::ast::Checker;
/// ## What it does /// ## What it does
/// Checks for redundant `Literal[None]` annotations. /// Checks for redundant `Literal[None]` annotations.
@ -225,10 +225,10 @@ fn create_fix(
let fix = match union_kind { let fix = match union_kind {
UnionKind::TypingOptional => { UnionKind::TypingOptional => {
let (import_edit, bound_name) = checker.importer().get_or_import_symbol( let (import_edit, bound_name) = checker.import_from_typing(
&ImportRequest::import_from("typing", "Optional"), "Optional",
literal_expr.start(), literal_expr.start(),
checker.semantic(), PythonVersion::lowest(),
)?; )?;
let optional_expr = typing_optional(new_literal_expr, Name::from(bound_name)); let optional_expr = typing_optional(new_literal_expr, Name::from(bound_name));
let content = checker.generator().expr(&optional_expr); let content = checker.generator().expr(&optional_expr);

View file

@ -6,12 +6,12 @@ use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Vi
use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{ use ruff_python_ast::{
name::Name, AnyParameterRef, Expr, ExprBinOp, ExprContext, ExprName, ExprSubscript, ExprTuple, name::Name, AnyParameterRef, Expr, ExprBinOp, ExprContext, ExprName, ExprSubscript, ExprTuple,
Operator, Parameters, Operator, Parameters, PythonVersion,
}; };
use ruff_python_semantic::analyze::typing::traverse_union; use ruff_python_semantic::analyze::typing::traverse_union;
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use crate::{checkers::ast::Checker, importer::ImportRequest}; use crate::checkers::ast::Checker;
/// ## What it does /// ## What it does
/// Checks for parameter annotations that contain redundant unions between /// Checks for parameter annotations that contain redundant unions between
@ -268,11 +268,8 @@ fn generate_union_fix(
debug_assert!(nodes.len() >= 2, "At least two nodes required"); debug_assert!(nodes.len() >= 2, "At least two nodes required");
// Request `typing.Union` // Request `typing.Union`
let (import_edit, binding) = checker.importer().get_or_import_symbol( let (import_edit, binding) =
&ImportRequest::import_from("typing", "Union"), checker.import_from_typing("Optional", annotation.start(), PythonVersion::lowest())?;
annotation.start(),
checker.semantic(),
)?;
// Construct the expression as `Subscript[typing.Union, Tuple[expr, [expr, ...]]]` // Construct the expression as `Subscript[typing.Union, Tuple[expr, [expr, ...]]]`
let new_expr = Expr::Subscript(ExprSubscript { let new_expr = Expr::Subscript(ExprSubscript {

View file

@ -6,7 +6,6 @@ use ruff_python_semantic::{analyze::class::is_enumeration, ScopeKind, SemanticMo
use ruff_text_size::Ranged; use ruff_text_size::Ranged;
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::rules::flake8_pyi::rules::TypingModule; use crate::rules::flake8_pyi::rules::TypingModule;
use crate::Locator; use crate::Locator;
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;
@ -682,11 +681,8 @@ pub(crate) fn type_alias_without_annotation(checker: &Checker, value: &Expr, tar
target.range(), target.range(),
); );
diagnostic.try_set_fix(|| { diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol( let (import_edit, binding) =
&ImportRequest::import(module.as_str(), "TypeAlias"), checker.import_from_typing("TypeAlias", target.start(), PythonVersion::PY310)?;
target.start(),
checker.semantic(),
)?;
Ok(Fix::safe_edits( Ok(Fix::safe_edits(
Edit::range_replacement(format!("{id}: {binding}"), target.range()), Edit::range_replacement(format!("{id}: {binding}"), target.range()),
[import_edit], [import_edit],

View file

@ -14,10 +14,10 @@ PYI026.pyi:3:1: PYI026 [*] Use `typing_extensions.TypeAlias` for type alias, e.g
Safe fix Safe fix
1 1 | from typing import Literal, Any 1 1 | from typing import Literal, Any
2 |+import typing_extensions 2 |+from typing_extensions import TypeAlias
2 3 | 2 3 |
3 |-NewAny = Any 3 |-NewAny = Any
4 |+NewAny: typing_extensions.TypeAlias = Any 4 |+NewAny: TypeAlias = Any
4 5 | OptionalStr = typing.Optional[str] 4 5 | OptionalStr = typing.Optional[str]
5 6 | Foo = Literal["foo"] 5 6 | Foo = Literal["foo"]
6 7 | IntOrStr = int | str 6 7 | IntOrStr = int | str
@ -34,11 +34,11 @@ PYI026.pyi:4:1: PYI026 [*] Use `typing_extensions.TypeAlias` for type alias, e.g
Safe fix Safe fix
1 1 | from typing import Literal, Any 1 1 | from typing import Literal, Any
2 |+import typing_extensions 2 |+from typing_extensions import TypeAlias
2 3 | 2 3 |
3 4 | NewAny = Any 3 4 | NewAny = Any
4 |-OptionalStr = typing.Optional[str] 4 |-OptionalStr = typing.Optional[str]
5 |+OptionalStr: typing_extensions.TypeAlias = typing.Optional[str] 5 |+OptionalStr: TypeAlias = typing.Optional[str]
5 6 | Foo = Literal["foo"] 5 6 | Foo = Literal["foo"]
6 7 | IntOrStr = int | str 6 7 | IntOrStr = int | str
7 8 | AliasNone = None 7 8 | AliasNone = None
@ -56,12 +56,12 @@ PYI026.pyi:5:1: PYI026 [*] Use `typing_extensions.TypeAlias` for type alias, e.g
Safe fix Safe fix
1 1 | from typing import Literal, Any 1 1 | from typing import Literal, Any
2 |+import typing_extensions 2 |+from typing_extensions import TypeAlias
2 3 | 2 3 |
3 4 | NewAny = Any 3 4 | NewAny = Any
4 5 | OptionalStr = typing.Optional[str] 4 5 | OptionalStr = typing.Optional[str]
5 |-Foo = Literal["foo"] 5 |-Foo = Literal["foo"]
6 |+Foo: typing_extensions.TypeAlias = Literal["foo"] 6 |+Foo: TypeAlias = Literal["foo"]
6 7 | IntOrStr = int | str 6 7 | IntOrStr = int | str
7 8 | AliasNone = None 7 8 | AliasNone = None
8 9 | 8 9 |
@ -78,13 +78,13 @@ PYI026.pyi:6:1: PYI026 [*] Use `typing_extensions.TypeAlias` for type alias, e.g
Safe fix Safe fix
1 1 | from typing import Literal, Any 1 1 | from typing import Literal, Any
2 |+import typing_extensions 2 |+from typing_extensions import TypeAlias
2 3 | 2 3 |
3 4 | NewAny = Any 3 4 | NewAny = Any
4 5 | OptionalStr = typing.Optional[str] 4 5 | OptionalStr = typing.Optional[str]
5 6 | Foo = Literal["foo"] 5 6 | Foo = Literal["foo"]
6 |-IntOrStr = int | str 6 |-IntOrStr = int | str
7 |+IntOrStr: typing_extensions.TypeAlias = int | str 7 |+IntOrStr: TypeAlias = int | str
7 8 | AliasNone = None 7 8 | AliasNone = None
8 9 | 8 9 |
9 10 | NewAny: typing.TypeAlias = Any 9 10 | NewAny: typing.TypeAlias = Any
@ -102,14 +102,14 @@ PYI026.pyi:7:1: PYI026 [*] Use `typing_extensions.TypeAlias` for type alias, e.g
Safe fix Safe fix
1 1 | from typing import Literal, Any 1 1 | from typing import Literal, Any
2 |+import typing_extensions 2 |+from typing_extensions import TypeAlias
2 3 | 2 3 |
3 4 | NewAny = Any 3 4 | NewAny = Any
4 5 | OptionalStr = typing.Optional[str] 4 5 | OptionalStr = typing.Optional[str]
5 6 | Foo = Literal["foo"] 5 6 | Foo = Literal["foo"]
6 7 | IntOrStr = int | str 6 7 | IntOrStr = int | str
7 |-AliasNone = None 7 |-AliasNone = None
8 |+AliasNone: typing_extensions.TypeAlias = None 8 |+AliasNone: TypeAlias = None
8 9 | 8 9 |
9 10 | NewAny: typing.TypeAlias = Any 9 10 | NewAny: typing.TypeAlias = Any
10 11 | OptionalStr: TypeAlias = typing.Optional[str] 10 11 | OptionalStr: TypeAlias = typing.Optional[str]
@ -126,7 +126,7 @@ PYI026.pyi:17:5: PYI026 [*] Use `typing_extensions.TypeAlias` for type alias, e.
Safe fix Safe fix
1 1 | from typing import Literal, Any 1 1 | from typing import Literal, Any
2 |+import typing_extensions 2 |+from typing_extensions import TypeAlias
2 3 | 2 3 |
3 4 | NewAny = Any 3 4 | NewAny = Any
4 5 | OptionalStr = typing.Optional[str] 4 5 | OptionalStr = typing.Optional[str]
@ -135,7 +135,7 @@ PYI026.pyi:17:5: PYI026 [*] Use `typing_extensions.TypeAlias` for type alias, e.
15 16 | 15 16 |
16 17 | class NotAnEnum: 16 17 | class NotAnEnum:
17 |- FLAG_THIS = None 17 |- FLAG_THIS = None
18 |+ FLAG_THIS: typing_extensions.TypeAlias = None 18 |+ FLAG_THIS: TypeAlias = None
18 19 | 18 19 |
19 20 | # these are ok 19 20 | # these are ok
20 21 | from enum import Enum 20 21 | from enum import Enum

View file

@ -10,7 +10,6 @@ use ruff_python_ast::{self as ast, Expr, Operator, Parameters};
use ruff_text_size::{Ranged, TextRange}; use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker; use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;
@ -137,11 +136,8 @@ fn generate_fix(checker: &Checker, conversion_type: ConversionType, expr: &Expr)
))) )))
} }
ConversionType::Optional => { ConversionType::Optional => {
let (import_edit, binding) = checker.importer().get_or_import_symbol( let (import_edit, binding) =
&ImportRequest::import_from("typing", "Optional"), checker.import_from_typing("Optional", expr.start(), PythonVersion::lowest())?;
expr.start(),
checker.semantic(),
)?;
let new_expr = Expr::Subscript(ast::ExprSubscript { let new_expr = Expr::Subscript(ast::ExprSubscript {
range: TextRange::default(), range: TextRange::default(),
value: Box::new(Expr::Name(ast::ExprName { value: Box::new(Expr::Name(ast::ExprName {

View file

@ -44,6 +44,11 @@ impl PythonVersion {
.into_iter() .into_iter()
} }
/// The minimum supported Python version.
pub const fn lowest() -> Self {
Self::PY37
}
pub const fn latest() -> Self { pub const fn latest() -> Self {
Self::PY313 Self::PY313
} }