diff --git a/crates/ruff/tests/lint.rs b/crates/ruff/tests/lint.rs index d58e85a468..77dcc222e7 100644 --- a/crates/ruff/tests/lint.rs +++ b/crates/ruff/tests/lint.rs @@ -994,6 +994,7 @@ fn value_given_to_table_key_is_not_inline_table_2() { - `lint.extend-per-file-ignores` - `lint.exclude` - `lint.preview` + - `lint.typing-extensions` For more information, try '--help'. "); @@ -2117,7 +2118,7 @@ requires-python = ">= 3.11" .arg("test.py") .arg("-") .current_dir(project_dir) - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -2207,6 +2208,7 @@ requires-python = ">= 3.11" XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -2390,7 +2392,7 @@ requires-python = ">= 3.11" analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) } @@ -2428,7 +2430,7 @@ requires-python = ">= 3.11" .arg("test.py") .arg("-") .current_dir(project_dir) - , @r###" + , @r#" success: true exit_code: 0 ----- stdout ----- @@ -2518,6 +2520,7 @@ requires-python = ">= 3.11" XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -2701,7 +2704,7 @@ requires-python = ">= 3.11" analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) } @@ -2790,7 +2793,7 @@ from typing import Union;foo: Union[int, str] = 1 .args(STDIN_BASE_OPTIONS) .arg("test.py") .arg("--show-settings") - .current_dir(project_dir), @r###" + .current_dir(project_dir), @r#" success: true exit_code: 0 ----- stdout ----- @@ -2881,6 +2884,7 @@ from typing import Union;foo: Union[int, str] = 1 XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -3064,7 +3068,7 @@ from typing import Union;foo: Union[int, str] = 1 analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) } @@ -3170,7 +3174,7 @@ from typing import Union;foo: Union[int, str] = 1 .arg("--show-settings") .args(["--select","UP007"]) .arg("foo/test.py") - .current_dir(&project_dir), @r###" + .current_dir(&project_dir), @r#" success: true exit_code: 0 ----- stdout ----- @@ -3260,6 +3264,7 @@ from typing import Union;foo: Union[int, str] = 1 XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -3443,7 +3448,7 @@ from typing import Union;foo: Union[int, str] = 1 analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) } @@ -3497,7 +3502,7 @@ from typing import Union;foo: Union[int, str] = 1 .arg("--show-settings") .args(["--select","UP007"]) .arg("foo/test.py") - .current_dir(&project_dir), @r###" + .current_dir(&project_dir), @r#" success: true exit_code: 0 ----- stdout ----- @@ -3587,6 +3592,7 @@ from typing import Union;foo: Union[int, str] = 1 XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -3770,7 +3776,7 @@ from typing import Union;foo: Union[int, str] = 1 analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) } @@ -3823,7 +3829,7 @@ from typing import Union;foo: Union[int, str] = 1 .args(STDIN_BASE_OPTIONS) .arg("--show-settings") .arg("foo/test.py") - .current_dir(&project_dir), @r###" + .current_dir(&project_dir), @r#" success: true exit_code: 0 ----- stdout ----- @@ -3914,6 +3920,7 @@ from typing import Union;foo: Union[int, str] = 1 XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -4097,7 +4104,7 @@ from typing import Union;foo: Union[int, str] = 1 analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); insta::with_settings!({ @@ -4107,7 +4114,7 @@ from typing import Union;foo: Union[int, str] = 1 .args(STDIN_BASE_OPTIONS) .arg("--show-settings") .arg("test.py") - .current_dir(project_dir.join("foo")), @r###" + .current_dir(project_dir.join("foo")), @r#" success: true exit_code: 0 ----- stdout ----- @@ -4198,6 +4205,7 @@ from typing import Union;foo: Union[int, str] = 1 XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -4381,7 +4389,7 @@ from typing import Union;foo: Union[int, str] = 1 analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) } @@ -4444,7 +4452,7 @@ from typing import Union;foo: Union[int, str] = 1 .args(STDIN_BASE_OPTIONS) .arg("--show-settings") .arg("test.py") - .current_dir(&project_dir), @r###" + .current_dir(&project_dir), @r#" success: true exit_code: 0 ----- stdout ----- @@ -4535,6 +4543,7 @@ from typing import Union;foo: Union[int, str] = 1 XXX, ] linter.typing_modules = [] + linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false @@ -4718,7 +4727,7 @@ from typing import Union;foo: Union[int, str] = 1 analyze.include_dependencies = {} ----- stderr ----- - "###); + "#); }); Ok(()) diff --git a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap index c404d07c8c..37e8eae6bc 100644 --- a/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap +++ b/crates/ruff/tests/snapshots/show_settings__display_default_settings.snap @@ -213,6 +213,7 @@ linter.task_tags = [ XXX, ] linter.typing_modules = [] +linter.typing_extensions = true # Linter Plugins linter.flake8_annotations.mypy_init_return = false diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 6611d27fa2..b647d9ac00 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -534,26 +534,50 @@ impl<'a> Checker<'a> { self.semantic_checker = checker; } - /// Attempt to create an [`Edit`] that imports `member`. + /// Create a [`TypingImporter`] that will import `member` from either `typing` or + /// `typing_extensions`. /// /// 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, + /// If the Python version is less than `version_added_to_typing` but + /// `LinterSettings::typing_extensions` is `false`, this method returns `None`. + pub(crate) fn typing_importer<'b>( + &'b self, + member: &'b str, version_added_to_typing: PythonVersion, - ) -> Result<(Edit, String), ResolutionError> { + ) -> Option> { let source_module = if self.target_version() >= version_added_to_typing { "typing" + } else if !self.settings.typing_extensions { + return None; } else { "typing_extensions" }; - let request = ImportRequest::import_from(source_module, member); - self.importer() - .get_or_import_symbol(&request, position, self.semantic()) + Some(TypingImporter { + checker: self, + source_module, + member, + }) + } +} + +pub(crate) struct TypingImporter<'a, 'b> { + checker: &'a Checker<'b>, + source_module: &'static str, + member: &'a str, +} + +impl TypingImporter<'_, '_> { + /// Create an [`Edit`] that makes the requested symbol available at `position`. + /// + /// See [`Importer::get_or_import_symbol`] for more details on the returned values and + /// [`Checker::typing_importer`] for a way to construct a [`TypingImporter`]. + pub(crate) fn import(&self, position: TextSize) -> Result<(Edit, String), ResolutionError> { + let request = ImportRequest::import_from(self.source_module, self.member); + self.checker + .importer + .get_or_import_symbol(&request, position, self.checker.semantic()) } } diff --git a/crates/ruff_linter/src/linter.rs b/crates/ruff_linter/src/linter.rs index 7f03f72e1a..00d07892dc 100644 --- a/crates/ruff_linter/src/linter.rs +++ b/crates/ruff_linter/src/linter.rs @@ -791,8 +791,9 @@ mod tests { use crate::linter::check_path; use crate::message::Message; use crate::registry::Rule; + use crate::settings::LinterSettings; use crate::source_kind::SourceKind; - use crate::test::{assert_notebook_path, test_contents, TestedNotebook}; + use crate::test::{assert_notebook_path, test_contents, test_snippet, TestedNotebook}; use crate::{assert_messages, directives, settings, Locator}; /// Construct a path to a Jupyter notebook in the `resources/test/fixtures/jupyter` directory. @@ -811,7 +812,7 @@ mod tests { } = assert_notebook_path( &actual, expected, - &settings::LinterSettings::for_rule(Rule::UnsortedImports), + &LinterSettings::for_rule(Rule::UnsortedImports), )?; assert_messages!(messages, actual, source_notebook); Ok(()) @@ -828,7 +829,7 @@ mod tests { } = assert_notebook_path( &actual, expected, - &settings::LinterSettings::for_rule(Rule::UnusedImport), + &LinterSettings::for_rule(Rule::UnusedImport), )?; assert_messages!(messages, actual, source_notebook); Ok(()) @@ -845,7 +846,7 @@ mod tests { } = assert_notebook_path( &actual, expected, - &settings::LinterSettings::for_rule(Rule::UnusedVariable), + &LinterSettings::for_rule(Rule::UnusedVariable), )?; assert_messages!(messages, actual, source_notebook); Ok(()) @@ -862,7 +863,7 @@ mod tests { } = assert_notebook_path( &actual, expected, - &settings::LinterSettings::for_rule(Rule::UndefinedName), + &LinterSettings::for_rule(Rule::UndefinedName), )?; assert_messages!(messages, actual, source_notebook); Ok(()) @@ -879,7 +880,7 @@ mod tests { } = assert_notebook_path( actual_path, &expected_path, - &settings::LinterSettings::for_rule(Rule::UnusedImport), + &LinterSettings::for_rule(Rule::UnusedImport), )?; let mut writer = Vec::new(); fixed_notebook.write(&mut writer)?; @@ -900,7 +901,7 @@ mod tests { } = assert_notebook_path( &actual, expected, - &settings::LinterSettings::for_rule(Rule::UnusedImport), + &LinterSettings::for_rule(Rule::UnusedImport), )?; assert_messages!(messages, actual, source_notebook); Ok(()) @@ -930,7 +931,7 @@ mod tests { let (_, transformed) = test_contents( &source_kind, path, - &settings::LinterSettings::for_rule(Rule::UnusedImport), + &LinterSettings::for_rule(Rule::UnusedImport), ); let linted_notebook = transformed.into_owned().expect_ipy_notebook(); let mut writer = Vec::new(); @@ -946,10 +947,7 @@ mod tests { /// Wrapper around `test_contents_syntax_errors` for testing a snippet of code instead of a /// file. - fn test_snippet_syntax_errors( - contents: &str, - settings: &settings::LinterSettings, - ) -> Vec { + fn test_snippet_syntax_errors(contents: &str, settings: &LinterSettings) -> Vec { let contents = dedent(contents); test_contents_syntax_errors( &SourceKind::Python(contents.to_string()), @@ -963,7 +961,7 @@ mod tests { fn test_contents_syntax_errors( source_kind: &SourceKind, path: &Path, - settings: &settings::LinterSettings, + settings: &LinterSettings, ) -> Vec { let source_type = PySourceType::from(path); let options = @@ -1032,7 +1030,7 @@ mod tests { let snapshot = format!("async_comprehension_in_sync_comprehension_{name}_{python_version}"); let messages = test_snippet_syntax_errors( contents, - &settings::LinterSettings { + &LinterSettings { rules: settings::rule_table::RuleTable::empty(), unresolved_target_version: python_version, preview: settings::types::PreviewMode::Enabled, @@ -1051,7 +1049,7 @@ mod tests { let messages = test_contents_syntax_errors( &SourceKind::IpyNotebook(Notebook::from_path(path)?), path, - &settings::LinterSettings { + &LinterSettings { unresolved_target_version: python_version, rules: settings::rule_table::RuleTable::empty(), preview: settings::types::PreviewMode::Enabled, @@ -1076,7 +1074,7 @@ mod tests { let messages = test_contents_syntax_errors( &SourceKind::Python(std::fs::read_to_string(&path)?), &path, - &settings::LinterSettings::for_rule(rule), + &LinterSettings::for_rule(rule), ); insta::with_settings!({filters => vec![(r"\\", "/")]}, { assert_messages!(snapshot, messages); @@ -1095,10 +1093,108 @@ mod tests { } = assert_notebook_path( path, path, - &settings::LinterSettings::for_rule(Rule::YieldOutsideFunction), + &LinterSettings::for_rule(Rule::YieldOutsideFunction), )?; assert_messages!(messages, path, source_notebook); Ok(()) } + + const PYI019_EXAMPLE: &str = r#" + from typing import TypeVar + + T = TypeVar("T", bound="_NiceReprEnum") + + class C: + def __new__(cls: type[T]) -> T: + return cls + "#; + + #[test_case( + "pyi019_adds_typing_extensions", + PYI019_EXAMPLE, + &LinterSettings { + unresolved_target_version: PythonVersion::PY310, + typing_extensions: true, + ..LinterSettings::for_rule(Rule::CustomTypeVarForSelf) + } + )] + #[test_case( + "pyi019_does_not_add_typing_extensions", + PYI019_EXAMPLE, + &LinterSettings { + unresolved_target_version: PythonVersion::PY310, + typing_extensions: false, + ..LinterSettings::for_rule(Rule::CustomTypeVarForSelf) + } + )] + #[test_case( + "pyi019_adds_typing_without_extensions_disabled", + PYI019_EXAMPLE, + &LinterSettings { + unresolved_target_version: PythonVersion::PY311, + typing_extensions: true, + ..LinterSettings::for_rule(Rule::CustomTypeVarForSelf) + } + )] + #[test_case( + "pyi019_adds_typing_with_extensions_disabled", + PYI019_EXAMPLE, + &LinterSettings { + unresolved_target_version: PythonVersion::PY311, + typing_extensions: false, + ..LinterSettings::for_rule(Rule::CustomTypeVarForSelf) + } + )] + #[test_case( + "pyi034_disabled", + " + class C: + def __new__(cls) -> C: ... + ", + &LinterSettings { + unresolved_target_version: PythonVersion { major: 3, minor: 10 }, + typing_extensions: false, + ..LinterSettings::for_rule(Rule::NonSelfReturnType) + } + )] + #[test_case( + "fast002_disabled", + r#" + from fastapi import Depends, FastAPI + + app = FastAPI() + + @app.get("/items/") + async def read_items(commons: dict = Depends(common_parameters)): + return commons + "#, + &LinterSettings { + unresolved_target_version: PythonVersion { major: 3, minor: 8 }, + typing_extensions: false, + ..LinterSettings::for_rule(Rule::FastApiNonAnnotatedDependency) + } + )] + fn test_disabled_typing_extensions(name: &str, contents: &str, settings: &LinterSettings) { + let snapshot = format!("disabled_typing_extensions_{name}"); + let messages = test_snippet(contents, settings); + assert_messages!(snapshot, messages); + } + + #[test_case( + "pyi026_disabled", + "Vector = list[float]", + &LinterSettings { + unresolved_target_version: PythonVersion { major: 3, minor: 9 }, + typing_extensions: false, + ..LinterSettings::for_rule(Rule::TypeAliasWithoutAnnotation) + } + )] + fn test_disabled_typing_extensions_pyi(name: &str, contents: &str, settings: &LinterSettings) { + let snapshot = format!("disabled_typing_extensions_pyi_{name}"); + let path = Path::new(".pyi"); + let contents = dedent(contents); + let messages = test_contents(&SourceKind::Python(contents.into_owned()), path, settings).0; + assert_messages!(snapshot, messages); + } } diff --git a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs index 9e537d398c..f9873e860b 100644 --- a/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs +++ b/crates/ruff_linter/src/rules/fastapi/rules/fastapi_non_annotated_dependency.rs @@ -59,6 +59,16 @@ use ruff_python_ast::PythonVersion; /// return commons /// ``` /// +/// ## Availability +/// +/// Because this rule relies on the third-party `typing_extensions` module for Python versions +/// before 3.9, its diagnostic will not be emitted, and no fix will be offered, if +/// `typing_extensions` imports have been disabled by the [`lint.typing-extensions`] linter option. +/// +/// ## Options +/// +/// - `lint.typing-extensions` +/// /// [FastAPI documentation]: https://fastapi.tiangolo.com/tutorial/query-params-str-validations/?h=annotated#advantages-of-annotated /// [typing-annotated]: https://docs.python.org/3/library/typing.html#typing.Annotated /// [typing-extensions]: https://typing-extensions.readthedocs.io/en/stable/ @@ -223,6 +233,10 @@ fn create_diagnostic( dependency_call: Option, mut seen_default: bool, ) -> bool { + let Some(importer) = checker.typing_importer("Annotated", PythonVersion::PY39) else { + return seen_default; + }; + let mut diagnostic = Diagnostic::new( FastApiNonAnnotatedDependency { py_version: checker.target_version(), @@ -231,11 +245,7 @@ fn create_diagnostic( ); let try_generate_fix = || { - let (import_edit, binding) = checker.import_from_typing( - "Annotated", - parameter.range.start(), - PythonVersion::PY39, - )?; + let (import_edit, binding) = importer.import(parameter.range.start())?; // Each of these classes takes a single, optional default // argument, followed by kw-only arguments diff --git a/crates/ruff_linter/src/rules/flake8_annotations/helpers.rs b/crates/ruff_linter/src/rules/flake8_annotations/helpers.rs index 1adf672cfe..3fb45451e2 100644 --- a/crates/ruff_linter/src/rules/flake8_annotations/helpers.rs +++ b/crates/ruff_linter/src/rules/flake8_annotations/helpers.rs @@ -131,7 +131,8 @@ impl AutoPythonType { "NoReturn" }; let (no_return_edit, binding) = checker - .import_from_typing(member, at, PythonVersion::lowest()) + .typing_importer(member, PythonVersion::lowest())? + .import(at) .ok()?; let expr = Expr::Name(ast::ExprName { id: Name::from(binding), @@ -169,7 +170,8 @@ impl AutoPythonType { // Ex) `Optional[int]` let (optional_edit, binding) = checker - .import_from_typing("Optional", at, PythonVersion::lowest()) + .typing_importer("Optional", PythonVersion::lowest())? + .import(at) .ok()?; let expr = typing_optional(element, Name::from(binding)); Some((expr, vec![optional_edit])) @@ -182,7 +184,8 @@ impl AutoPythonType { // Ex) `Union[int, str]` let (union_edit, binding) = checker - .import_from_typing("Union", at, PythonVersion::lowest()) + .typing_importer("Union", PythonVersion::lowest())? + .import(at) .ok()?; let expr = typing_union(&elements, Name::from(binding)); Some((expr, vec![union_edit])) diff --git a/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs index 786877146c..e9560cfd93 100644 --- a/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff_linter/src/rules/flake8_annotations/rules/definition.rs @@ -224,6 +224,16 @@ impl Violation for MissingTypeCls { /// def add(a: int, b: int) -> int: /// return a + b /// ``` +/// +/// ## Availability +/// +/// Because this rule relies on the third-party `typing_extensions` module for some Python versions, +/// its diagnostic will not be emitted, and no fix will be offered, if `typing_extensions` imports +/// have been disabled by the [`lint.typing-extensions`] linter option. +/// +/// ## Options +/// +/// - `lint.typing-extensions` #[derive(ViolationMetadata)] pub(crate) struct MissingReturnTypeUndocumentedPublicFunction { name: String, @@ -267,6 +277,16 @@ impl Violation for MissingReturnTypeUndocumentedPublicFunction { /// def _add(a: int, b: int) -> int: /// return a + b /// ``` +/// +/// ## Availability +/// +/// Because this rule relies on the third-party `typing_extensions` module for some Python versions, +/// its diagnostic will not be emitted, and no fix will be offered, if `typing_extensions` imports +/// have been disabled by the [`lint.typing-extensions`] linter option. +/// +/// ## Options +/// +/// - `lint.typing-extensions` #[derive(ViolationMetadata)] pub(crate) struct MissingReturnTypePrivateFunction { name: String, diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs index e3b84a605a..cfadfcc97c 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/custom_type_var_for_self.rs @@ -10,7 +10,7 @@ use ruff_python_semantic::analyze::visibility::{is_abstract, is_overload}; use ruff_python_semantic::{Binding, ResolvedReference, ScopeId, SemanticModel}; use ruff_text_size::{Ranged, TextRange}; -use crate::checkers::ast::Checker; +use crate::checkers::ast::{Checker, TypingImporter}; use ruff_python_ast::PythonVersion; /// ## What it does @@ -70,6 +70,16 @@ use ruff_python_ast::PythonVersion; /// The fix is only marked as unsafe if there is the possibility that it might delete a comment /// from your code. /// +/// ## Availability +/// +/// Because this rule relies on the third-party `typing_extensions` module for Python versions +/// before 3.11, its diagnostic will not be emitted, and no fix will be offered, if +/// `typing_extensions` imports have been disabled by the [`lint.typing-extensions`] linter option. +/// +/// ## Options +/// +/// - `lint.typing-extensions` +/// /// [PEP 673]: https://peps.python.org/pep-0673/#motivation /// [PEP-695]: https://peps.python.org/pep-0695/ /// [PYI018]: https://docs.astral.sh/ruff/rules/unused-private-type-var/ @@ -109,6 +119,7 @@ pub(crate) fn custom_type_var_instead_of_self( let semantic = checker.semantic(); let current_scope = &semantic.scopes[binding.scope]; let function_def = binding.statement(semantic)?.as_function_def_stmt()?; + let importer = checker.typing_importer("Self", PythonVersion::PY311)?; let ast::StmtFunctionDef { name: function_name, @@ -178,6 +189,7 @@ pub(crate) fn custom_type_var_instead_of_self( diagnostic.try_set_fix(|| { replace_custom_typevar_with_self( checker, + &importer, function_def, custom_typevar, self_or_cls_parameter, @@ -310,14 +322,14 @@ fn custom_typevar<'a>( /// * If it was a PEP-695 type variable, removes that `TypeVar` from the PEP-695 type-parameter list fn replace_custom_typevar_with_self( checker: &Checker, + importer: &TypingImporter, function_def: &ast::StmtFunctionDef, custom_typevar: TypeVar, self_or_cls_parameter: &ast::ParameterWithDefault, self_or_cls_annotation: &ast::Expr, ) -> anyhow::Result { // (1) Import `Self` (if necessary) - let (import_edit, self_symbol_binding) = - checker.import_from_typing("Self", function_def.start(), PythonVersion::PY311)?; + let (import_edit, self_symbol_binding) = importer.import(function_def.start())?; // (2) Remove the first parameter's annotation let mut other_edits = vec![Edit::deletion( diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_union_member.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_union_member.rs index 2ddc53ba1e..0c11167a91 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_union_member.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/duplicate_union_member.rs @@ -1,21 +1,18 @@ use std::collections::HashSet; -use anyhow::Result; - use rustc_hash::FxHashSet; use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast::comparable::ComparableExpr; -use ruff_python_ast::name::Name; -use ruff_python_ast::{ - Expr, ExprBinOp, ExprContext, ExprName, ExprSubscript, ExprTuple, Operator, PythonVersion, -}; +use ruff_python_ast::{Expr, ExprBinOp, Operator, PythonVersion}; use ruff_python_semantic::analyze::typing::traverse_union; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use super::generate_union_fix; + /// ## What it does /// Checks for duplicate union members. /// @@ -118,7 +115,19 @@ pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) { applicability, )), UnionKind::TypingUnion => { - generate_union_fix(checker, unique_nodes, expr, applicability).ok() + // Request `typing.Union` + let Some(importer) = checker.typing_importer("Union", PythonVersion::lowest()) + else { + return; + }; + generate_union_fix( + checker.generator(), + &importer, + unique_nodes, + expr, + applicability, + ) + .ok() } } }; @@ -171,40 +180,3 @@ fn generate_pep604_fix( applicability, ) } - -/// Generate a [`Fix`] for two or more type expressions, e.g. `typing.Union[int, float, complex]`. -fn generate_union_fix( - checker: &Checker, - nodes: Vec<&Expr>, - annotation: &Expr, - applicability: Applicability, -) -> Result { - debug_assert!(nodes.len() >= 2, "At least two nodes required"); - - // Request `typing.Union` - let (import_edit, binding) = - checker.import_from_typing("Union", annotation.start(), PythonVersion::lowest())?; - - // Construct the expression as `Subscript[typing.Union, Tuple[expr, [expr, ...]]]` - let new_expr = Expr::Subscript(ExprSubscript { - range: TextRange::default(), - value: Box::new(Expr::Name(ExprName { - id: Name::new(binding), - ctx: ExprContext::Store, - range: TextRange::default(), - })), - slice: Box::new(Expr::Tuple(ExprTuple { - elts: nodes.into_iter().cloned().collect(), - range: TextRange::default(), - ctx: ExprContext::Load, - parenthesized: false, - })), - ctx: ExprContext::Load, - }); - - Ok(Fix::applicable_edits( - Edit::range_replacement(checker.generator().expr(&new_expr), annotation.range()), - [import_edit], - applicability, - )) -} diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/mod.rs index 25be942f19..ca7aadec7f 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/mod.rs @@ -1,5 +1,14 @@ use std::fmt; +use anyhow::Result; + +use ruff_diagnostics::{Applicability, Edit, Fix}; +use ruff_python_ast::{name::Name, Expr, ExprContext, ExprName, ExprSubscript, ExprTuple}; +use ruff_python_codegen::Generator; +use ruff_text_size::{Ranged, TextRange}; + +use crate::checkers::ast::TypingImporter; + pub(crate) use any_eq_ne_annotation::*; pub(crate) use bad_generator_return_type::*; pub(crate) use bad_version_info_comparison::*; @@ -108,3 +117,39 @@ impl fmt::Display for TypingModule { fmt.write_str(self.as_str()) } } + +/// Generate a [`Fix`] for two or more type expressions, e.g. `typing.Union[int, float, complex]`. +fn generate_union_fix( + generator: Generator, + importer: &TypingImporter, + nodes: Vec<&Expr>, + annotation: &Expr, + applicability: Applicability, +) -> Result { + debug_assert!(nodes.len() >= 2, "At least two nodes required"); + + let (import_edit, binding) = importer.import(annotation.start())?; + + // Construct the expression as `Subscript[typing.Union, Tuple[expr, [expr, ...]]]` + let new_expr = Expr::Subscript(ExprSubscript { + range: TextRange::default(), + value: Box::new(Expr::Name(ExprName { + id: Name::new(binding), + ctx: ExprContext::Store, + range: TextRange::default(), + })), + slice: Box::new(Expr::Tuple(ExprTuple { + elts: nodes.into_iter().cloned().collect(), + range: TextRange::default(), + ctx: ExprContext::Load, + parenthesized: false, + })), + ctx: ExprContext::Load, + }); + + Ok(Fix::applicable_edits( + Edit::range_replacement(generator.expr(&new_expr), annotation.range()), + [import_edit], + applicability, + )) +} diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs index 1eda92b7ee..838c015454 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/non_self_return_type.rs @@ -1,4 +1,4 @@ -use crate::checkers::ast::Checker; +use crate::checkers::ast::{Checker, TypingImporter}; use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast as ast; @@ -75,6 +75,16 @@ use ruff_text_size::Ranged; /// ## Fix safety /// This rule's fix is marked as unsafe as it changes the meaning of your type annotations. /// +/// ## Availability +/// +/// Because this rule relies on the third-party `typing_extensions` module for Python versions +/// before 3.11, its diagnostic will not be emitted, and no fix will be offered, if +/// `typing_extensions` imports have been disabled by the [`lint.typing-extensions`] linter option. +/// +/// ## Options +/// +/// - `lint.typing-extensions` +/// /// ## References /// - [Python documentation: `typing.Self`](https://docs.python.org/3/library/typing.html#typing.Self) #[derive(ViolationMetadata)] @@ -192,6 +202,10 @@ fn add_diagnostic( class_def: &ast::StmtClassDef, method_name: &str, ) { + let Some(importer) = checker.typing_importer("Self", PythonVersion::PY311) else { + return; + }; + let mut diagnostic = Diagnostic::new( NonSelfReturnType { class_name: class_def.name.to_string(), @@ -200,21 +214,21 @@ fn add_diagnostic( stmt.identifier(), ); - diagnostic.try_set_fix(|| replace_with_self_fix(checker, stmt, returns, class_def)); + diagnostic.try_set_fix(|| { + replace_with_self_fix(checker.semantic(), &importer, stmt, returns, class_def) + }); checker.report_diagnostic(diagnostic); } fn replace_with_self_fix( - checker: &Checker, + semantic: &SemanticModel, + importer: &TypingImporter, stmt: &ast::Stmt, returns: &ast::Expr, class_def: &ast::StmtClassDef, ) -> anyhow::Result { - let semantic = checker.semantic(); - - let (self_import, self_binding) = - checker.import_from_typing("Self", returns.start(), PythonVersion::PY311)?; + let (self_import, self_binding) = importer.import(returns.start())?; let mut others = Vec::with_capacity(2); @@ -230,7 +244,7 @@ fn replace_with_self_fix( others.extend(remove_first_argument_type_hint()); others.push(Edit::range_replacement(self_binding, returns.range())); - let applicability = if might_be_generic(class_def, checker.semantic()) { + let applicability = if might_be_generic(class_def, semantic) { Applicability::DisplayOnly } else { Applicability::Unsafe diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs index 07ca4a96c2..5a918c41b6 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_none_literal.rs @@ -222,11 +222,11 @@ fn create_fix( let fix = match union_kind { UnionKind::TypingOptional => { - let (import_edit, bound_name) = checker.import_from_typing( - "Optional", - literal_expr.start(), - PythonVersion::lowest(), - )?; + let Some(importer) = checker.typing_importer("Optional", PythonVersion::lowest()) + else { + return Ok(None); + }; + let (import_edit, bound_name) = importer.import(literal_expr.start())?; let optional_expr = typing_optional(new_literal_expr, Name::from(bound_name)); let content = checker.generator().expr(&optional_expr); let optional_edit = Edit::range_replacement(content, literal_expr.range()); diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs index 2f196c2caf..d8454b405b 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/redundant_numeric_union.rs @@ -1,18 +1,15 @@ use bitflags::bitflags; -use anyhow::Result; - use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; -use ruff_python_ast::{ - name::Name, AnyParameterRef, Expr, ExprBinOp, ExprContext, ExprName, ExprSubscript, ExprTuple, - Operator, Parameters, PythonVersion, -}; +use ruff_python_ast::{AnyParameterRef, Expr, ExprBinOp, Operator, Parameters, PythonVersion}; use ruff_python_semantic::analyze::typing::traverse_union; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use super::generate_union_fix; + /// ## What it does /// Checks for parameter annotations that contain redundant unions between /// builtin numeric types (e.g., `int | float`). @@ -157,7 +154,18 @@ fn check_annotation<'a>(checker: &Checker, annotation: &'a Expr) { applicability, )), UnionKind::TypingUnion => { - generate_union_fix(checker, necessary_nodes, annotation, applicability).ok() + let Some(importer) = checker.typing_importer("Union", PythonVersion::lowest()) + else { + return; + }; + generate_union_fix( + checker.generator(), + &importer, + necessary_nodes, + annotation, + applicability, + ) + .ok() } } }; @@ -257,40 +265,3 @@ fn generate_pep604_fix( applicability, ) } - -/// Generate a [`Fix`] for two or more type expressions, e.g. `typing.Union[int, float, complex]`. -fn generate_union_fix( - checker: &Checker, - nodes: Vec<&Expr>, - annotation: &Expr, - applicability: Applicability, -) -> Result { - debug_assert!(nodes.len() >= 2, "At least two nodes required"); - - // Request `typing.Union` - let (import_edit, binding) = - checker.import_from_typing("Optional", annotation.start(), PythonVersion::lowest())?; - - // Construct the expression as `Subscript[typing.Union, Tuple[expr, [expr, ...]]]` - let new_expr = Expr::Subscript(ExprSubscript { - range: TextRange::default(), - value: Box::new(Expr::Name(ExprName { - id: Name::new(binding), - ctx: ExprContext::Store, - range: TextRange::default(), - })), - slice: Box::new(Expr::Tuple(ExprTuple { - elts: nodes.into_iter().cloned().collect(), - range: TextRange::default(), - ctx: ExprContext::Load, - parenthesized: false, - })), - ctx: ExprContext::Load, - }); - - Ok(Fix::applicable_edits( - Edit::range_replacement(checker.generator().expr(&new_expr), annotation.range()), - [import_edit], - applicability, - )) -} diff --git a/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs b/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs index 3a2c05e4d1..11719dedcb 100644 --- a/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs +++ b/crates/ruff_linter/src/rules/flake8_pyi/rules/simple_defaults.rs @@ -217,6 +217,16 @@ impl Violation for UnassignedSpecialVariableInStub { /// /// Vector: TypeAlias = list[float] /// ``` +/// +/// ## Availability +/// +/// Because this rule relies on the third-party `typing_extensions` module for Python versions +/// before 3.10, its diagnostic will not be emitted, and no fix will be offered, if +/// `typing_extensions` imports have been disabled by the [`lint.typing-extensions`] linter option. +/// +/// ## Options +/// +/// - `lint.typing-extensions` #[derive(ViolationMetadata)] pub(crate) struct TypeAliasWithoutAnnotation { module: TypingModule, @@ -672,6 +682,10 @@ pub(crate) fn type_alias_without_annotation(checker: &Checker, value: &Expr, tar TypingModule::TypingExtensions }; + let Some(importer) = checker.typing_importer("TypeAlias", PythonVersion::PY310) else { + return; + }; + let mut diagnostic = Diagnostic::new( TypeAliasWithoutAnnotation { module, @@ -681,8 +695,7 @@ pub(crate) fn type_alias_without_annotation(checker: &Checker, value: &Expr, tar target.range(), ); diagnostic.try_set_fix(|| { - let (import_edit, binding) = - checker.import_from_typing("TypeAlias", target.start(), PythonVersion::PY310)?; + let (import_edit, binding) = importer.import(target.start())?; Ok(Fix::safe_edits( Edit::range_replacement(format!("{id}: {binding}"), target.range()), [import_edit], diff --git a/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs b/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs index c59a94b1a1..e1a9d6f7bd 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/implicit_optional.rs @@ -1,6 +1,6 @@ use std::fmt; -use anyhow::Result; +use anyhow::{Context, Result}; use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_macros::{derive_message_formats, ViolationMetadata}; @@ -136,8 +136,10 @@ fn generate_fix(checker: &Checker, conversion_type: ConversionType, expr: &Expr) ))) } ConversionType::Optional => { - let (import_edit, binding) = - checker.import_from_typing("Optional", expr.start(), PythonVersion::lowest())?; + let importer = checker + .typing_importer("Optional", PythonVersion::lowest()) + .context("Optional should be available on all supported Python versions")?; + let (import_edit, binding) = importer.import(expr.start())?; let new_expr = Expr::Subscript(ast::ExprSubscript { range: TextRange::default(), value: Box::new(Expr::Name(ast::ExprName { diff --git a/crates/ruff_linter/src/settings/mod.rs b/crates/ruff_linter/src/settings/mod.rs index e4aff6e749..6b9c19e8a9 100644 --- a/crates/ruff_linter/src/settings/mod.rs +++ b/crates/ruff_linter/src/settings/mod.rs @@ -250,6 +250,7 @@ pub struct LinterSettings { pub line_length: LineLength, pub task_tags: Vec, pub typing_modules: Vec, + pub typing_extensions: bool, // Plugins pub flake8_annotations: flake8_annotations::settings::Settings, @@ -313,6 +314,7 @@ impl Display for LinterSettings { self.line_length, self.task_tags | array, self.typing_modules | array, + self.typing_extensions, ] } writeln!(f, "\n# Linter Plugins")?; @@ -450,6 +452,7 @@ impl LinterSettings { preview: PreviewMode::default(), explicit_preview_rules: false, extension: ExtensionMapping::default(), + typing_extensions: true, } } diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_fast002_disabled.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_fast002_disabled.snap new file mode 100644 index 0000000000..4ba33c756c --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_fast002_disabled.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- + diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_extensions.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_extensions.snap new file mode 100644 index 0000000000..b2537598f0 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_extensions.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:7:13: PYI019 [*] Use `Self` instead of custom TypeVar `T` + | +6 | class C: +7 | def __new__(cls: type[T]) -> T: + | ^^^^^^^^^^^^^^^^^^^ PYI019 +8 | return cls + | + = help: Replace TypeVar `T` with `Self` + +ℹ Safe fix +1 1 | +2 2 | from typing import TypeVar + 3 |+from typing_extensions import Self +3 4 | +4 5 | T = TypeVar("T", bound="_NiceReprEnum") +5 6 | +6 7 | class C: +7 |- def __new__(cls: type[T]) -> T: + 8 |+ def __new__(cls) -> Self: +8 9 | return cls diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_with_extensions_disabled.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_with_extensions_disabled.snap new file mode 100644 index 0000000000..0dee5cdff1 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_with_extensions_disabled.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:7:13: PYI019 [*] Use `Self` instead of custom TypeVar `T` + | +6 | class C: +7 | def __new__(cls: type[T]) -> T: + | ^^^^^^^^^^^^^^^^^^^ PYI019 +8 | return cls + | + = help: Replace TypeVar `T` with `Self` + +ℹ Safe fix +1 1 | +2 |-from typing import TypeVar + 2 |+from typing import TypeVar, Self +3 3 | +4 4 | T = TypeVar("T", bound="_NiceReprEnum") +5 5 | +6 6 | class C: +7 |- def __new__(cls: type[T]) -> T: + 7 |+ def __new__(cls) -> Self: +8 8 | return cls diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_without_extensions_disabled.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_without_extensions_disabled.snap new file mode 100644 index 0000000000..0dee5cdff1 --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_adds_typing_without_extensions_disabled.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- +:7:13: PYI019 [*] Use `Self` instead of custom TypeVar `T` + | +6 | class C: +7 | def __new__(cls: type[T]) -> T: + | ^^^^^^^^^^^^^^^^^^^ PYI019 +8 | return cls + | + = help: Replace TypeVar `T` with `Self` + +ℹ Safe fix +1 1 | +2 |-from typing import TypeVar + 2 |+from typing import TypeVar, Self +3 3 | +4 4 | T = TypeVar("T", bound="_NiceReprEnum") +5 5 | +6 6 | class C: +7 |- def __new__(cls: type[T]) -> T: + 7 |+ def __new__(cls) -> Self: +8 8 | return cls diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_does_not_add_typing_extensions.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_does_not_add_typing_extensions.snap new file mode 100644 index 0000000000..4ba33c756c --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi019_does_not_add_typing_extensions.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- + diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi034_disabled.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi034_disabled.snap new file mode 100644 index 0000000000..4ba33c756c --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi034_disabled.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- + diff --git a/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi_pyi026_disabled.snap b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi_pyi026_disabled.snap new file mode 100644 index 0000000000..4ba33c756c --- /dev/null +++ b/crates/ruff_linter/src/snapshots/ruff_linter__linter__tests__disabled_typing_extensions_pyi_pyi026_disabled.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff_linter/src/linter.rs +--- + diff --git a/crates/ruff_workspace/src/configuration.rs b/crates/ruff_workspace/src/configuration.rs index 260f4edc68..e311a0bf85 100644 --- a/crates/ruff_workspace/src/configuration.rs +++ b/crates/ruff_workspace/src/configuration.rs @@ -430,6 +430,7 @@ impl Configuration { .ruff .map(RuffOptions::into_settings) .unwrap_or_default(), + typing_extensions: lint.typing_extensions.unwrap_or(true), }, formatter, @@ -633,6 +634,7 @@ pub struct LintConfiguration { pub logger_objects: Option>, pub task_tags: Option>, pub typing_modules: Option>, + pub typing_extensions: Option, // Plugins pub flake8_annotations: Option, @@ -746,6 +748,7 @@ impl LintConfiguration { task_tags: options.common.task_tags, logger_objects: options.common.logger_objects, typing_modules: options.common.typing_modules, + typing_extensions: options.typing_extensions, // Plugins flake8_annotations: options.common.flake8_annotations, @@ -1170,6 +1173,7 @@ impl LintConfiguration { pylint: self.pylint.combine(config.pylint), pyupgrade: self.pyupgrade.combine(config.pyupgrade), ruff: self.ruff.combine(config.ruff), + typing_extensions: self.typing_extensions, } } } diff --git a/crates/ruff_workspace/src/options.rs b/crates/ruff_workspace/src/options.rs index fa83cd684d..6778c2682e 100644 --- a/crates/ruff_workspace/src/options.rs +++ b/crates/ruff_workspace/src/options.rs @@ -513,6 +513,22 @@ pub struct LintOptions { "# )] pub preview: Option, + + /// Whether to allow imports from the third-party `typing_extensions` module for Python versions + /// before a symbol was added to the first-party `typing` module. + /// + /// Many rules try to import symbols from the `typing` module but fall back to + /// `typing_extensions` for earlier versions of Python. This option can be used to disable this + /// fallback behavior in cases where `typing_extensions` is not installed. + #[option( + default = "true", + value_type = "bool", + example = r#" + # Disable `typing_extensions` imports + typing-extensions = false + "# + )] + pub typing_extensions: Option, } /// Newtype wrapper for [`LintCommonOptions`] that allows customizing the JSON schema and omitting the fields from the [`OptionsMetadata`]. @@ -3876,6 +3892,7 @@ pub struct LintOptionsWire { pydoclint: Option, ruff: Option, preview: Option, + typing_extensions: Option, } impl From for LintOptions { @@ -3930,6 +3947,7 @@ impl From for LintOptions { pydoclint, ruff, preview, + typing_extensions, } = value; LintOptions { @@ -3985,6 +4003,7 @@ impl From for LintOptions { pydoclint, ruff, preview, + typing_extensions, } } } diff --git a/ruff.schema.json b/ruff.schema.json index cbe8fa22cb..096df71618 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -2459,6 +2459,13 @@ "type": "string" } }, + "typing-extensions": { + "description": "Whether to allow imports from the third-party `typing_extensions` module for Python versions before a symbol was added to the first-party `typing` module.\n\nMany rules try to import symbols from the `typing` module but fall back to `typing_extensions` for earlier versions of Python. This option can be used to disable this fallback behavior in cases where `typing_extensions` is not installed.", + "type": [ + "boolean", + "null" + ] + }, "typing-modules": { "description": "A list of modules whose exports should be treated equivalently to members of the `typing` module.\n\nThis is useful for ensuring proper type annotation inference for projects that re-export `typing` and `typing_extensions` members from a compatibility module. If omitted, any members imported from modules apart from `typing` and `typing_extensions` will be treated as ordinary Python objects.", "type": [