Add config option to disable typing_extensions imports (#17611)

Summary
--

This PR resolves https://github.com/astral-sh/ruff/issues/9761 by adding
a linter configuration option to disable
`typing_extensions` imports. As mentioned [here], it would be ideal if
we could
detect whether or not `typing_extensions` is available as a dependency
automatically, but this seems like a much easier fix in the meantime.

The default for the new option, `typing-extensions`, is `true`,
preserving the current behavior. Setting it to `false` will bail out of
the new
`Checker::typing_importer` method, which has been refactored from the 
`Checker::import_from_typing` method in
https://github.com/astral-sh/ruff/pull/17340),
with `None`, which is then handled specially by each rule that calls it.

I considered some alternatives to a config option, such as checking if
`typing_extensions` has been imported or checking for a `TYPE_CHECKING`
block we could use, but I think defaulting to allowing
`typing_extensions` imports and allowing the user to disable this with
an option is both simple to implement and pretty intuitive.

[here]:
https://github.com/astral-sh/ruff/issues/9761#issuecomment-2790492853

Test Plan
--

New linter tests exercising several combinations of Python versions and
the new config option for PYI019. I also added tests for the other
affected rules, but only in the case where the new config option is
enabled. The rules' existing tests also cover the default case.
This commit is contained in:
Brent Westbrook 2025-04-28 14:57:36 -04:00 committed by GitHub
parent 405878a128
commit 01a31c08f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 470 additions and 160 deletions

View file

@ -994,6 +994,7 @@ fn value_given_to_table_key_is_not_inline_table_2() {
- `lint.extend-per-file-ignores` - `lint.extend-per-file-ignores`
- `lint.exclude` - `lint.exclude`
- `lint.preview` - `lint.preview`
- `lint.typing-extensions`
For more information, try '--help'. For more information, try '--help'.
"); ");
@ -2117,7 +2118,7 @@ requires-python = ">= 3.11"
.arg("test.py") .arg("test.py")
.arg("-") .arg("-")
.current_dir(project_dir) .current_dir(project_dir)
, @r###" , @r#"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -2207,6 +2208,7 @@ requires-python = ">= 3.11"
XXX, XXX,
] ]
linter.typing_modules = [] linter.typing_modules = []
linter.typing_extensions = true
# Linter Plugins # Linter Plugins
linter.flake8_annotations.mypy_init_return = false linter.flake8_annotations.mypy_init_return = false
@ -2390,7 +2392,7 @@ requires-python = ">= 3.11"
analyze.include_dependencies = {} analyze.include_dependencies = {}
----- stderr ----- ----- stderr -----
"###); "#);
}); });
Ok(()) Ok(())
} }
@ -2428,7 +2430,7 @@ requires-python = ">= 3.11"
.arg("test.py") .arg("test.py")
.arg("-") .arg("-")
.current_dir(project_dir) .current_dir(project_dir)
, @r###" , @r#"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -2518,6 +2520,7 @@ requires-python = ">= 3.11"
XXX, XXX,
] ]
linter.typing_modules = [] linter.typing_modules = []
linter.typing_extensions = true
# Linter Plugins # Linter Plugins
linter.flake8_annotations.mypy_init_return = false linter.flake8_annotations.mypy_init_return = false
@ -2701,7 +2704,7 @@ requires-python = ">= 3.11"
analyze.include_dependencies = {} analyze.include_dependencies = {}
----- stderr ----- ----- stderr -----
"###); "#);
}); });
Ok(()) Ok(())
} }
@ -2790,7 +2793,7 @@ from typing import Union;foo: Union[int, str] = 1
.args(STDIN_BASE_OPTIONS) .args(STDIN_BASE_OPTIONS)
.arg("test.py") .arg("test.py")
.arg("--show-settings") .arg("--show-settings")
.current_dir(project_dir), @r###" .current_dir(project_dir), @r#"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -2881,6 +2884,7 @@ from typing import Union;foo: Union[int, str] = 1
XXX, XXX,
] ]
linter.typing_modules = [] linter.typing_modules = []
linter.typing_extensions = true
# Linter Plugins # Linter Plugins
linter.flake8_annotations.mypy_init_return = false linter.flake8_annotations.mypy_init_return = false
@ -3064,7 +3068,7 @@ from typing import Union;foo: Union[int, str] = 1
analyze.include_dependencies = {} analyze.include_dependencies = {}
----- stderr ----- ----- stderr -----
"###); "#);
}); });
Ok(()) Ok(())
} }
@ -3170,7 +3174,7 @@ from typing import Union;foo: Union[int, str] = 1
.arg("--show-settings") .arg("--show-settings")
.args(["--select","UP007"]) .args(["--select","UP007"])
.arg("foo/test.py") .arg("foo/test.py")
.current_dir(&project_dir), @r###" .current_dir(&project_dir), @r#"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -3260,6 +3264,7 @@ from typing import Union;foo: Union[int, str] = 1
XXX, XXX,
] ]
linter.typing_modules = [] linter.typing_modules = []
linter.typing_extensions = true
# Linter Plugins # Linter Plugins
linter.flake8_annotations.mypy_init_return = false linter.flake8_annotations.mypy_init_return = false
@ -3443,7 +3448,7 @@ from typing import Union;foo: Union[int, str] = 1
analyze.include_dependencies = {} analyze.include_dependencies = {}
----- stderr ----- ----- stderr -----
"###); "#);
}); });
Ok(()) Ok(())
} }
@ -3497,7 +3502,7 @@ from typing import Union;foo: Union[int, str] = 1
.arg("--show-settings") .arg("--show-settings")
.args(["--select","UP007"]) .args(["--select","UP007"])
.arg("foo/test.py") .arg("foo/test.py")
.current_dir(&project_dir), @r###" .current_dir(&project_dir), @r#"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -3587,6 +3592,7 @@ from typing import Union;foo: Union[int, str] = 1
XXX, XXX,
] ]
linter.typing_modules = [] linter.typing_modules = []
linter.typing_extensions = true
# Linter Plugins # Linter Plugins
linter.flake8_annotations.mypy_init_return = false linter.flake8_annotations.mypy_init_return = false
@ -3770,7 +3776,7 @@ from typing import Union;foo: Union[int, str] = 1
analyze.include_dependencies = {} analyze.include_dependencies = {}
----- stderr ----- ----- stderr -----
"###); "#);
}); });
Ok(()) Ok(())
} }
@ -3823,7 +3829,7 @@ from typing import Union;foo: Union[int, str] = 1
.args(STDIN_BASE_OPTIONS) .args(STDIN_BASE_OPTIONS)
.arg("--show-settings") .arg("--show-settings")
.arg("foo/test.py") .arg("foo/test.py")
.current_dir(&project_dir), @r###" .current_dir(&project_dir), @r#"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -3914,6 +3920,7 @@ from typing import Union;foo: Union[int, str] = 1
XXX, XXX,
] ]
linter.typing_modules = [] linter.typing_modules = []
linter.typing_extensions = true
# Linter Plugins # Linter Plugins
linter.flake8_annotations.mypy_init_return = false linter.flake8_annotations.mypy_init_return = false
@ -4097,7 +4104,7 @@ from typing import Union;foo: Union[int, str] = 1
analyze.include_dependencies = {} analyze.include_dependencies = {}
----- stderr ----- ----- stderr -----
"###); "#);
}); });
insta::with_settings!({ insta::with_settings!({
@ -4107,7 +4114,7 @@ from typing import Union;foo: Union[int, str] = 1
.args(STDIN_BASE_OPTIONS) .args(STDIN_BASE_OPTIONS)
.arg("--show-settings") .arg("--show-settings")
.arg("test.py") .arg("test.py")
.current_dir(project_dir.join("foo")), @r###" .current_dir(project_dir.join("foo")), @r#"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -4198,6 +4205,7 @@ from typing import Union;foo: Union[int, str] = 1
XXX, XXX,
] ]
linter.typing_modules = [] linter.typing_modules = []
linter.typing_extensions = true
# Linter Plugins # Linter Plugins
linter.flake8_annotations.mypy_init_return = false linter.flake8_annotations.mypy_init_return = false
@ -4381,7 +4389,7 @@ from typing import Union;foo: Union[int, str] = 1
analyze.include_dependencies = {} analyze.include_dependencies = {}
----- stderr ----- ----- stderr -----
"###); "#);
}); });
Ok(()) Ok(())
} }
@ -4444,7 +4452,7 @@ from typing import Union;foo: Union[int, str] = 1
.args(STDIN_BASE_OPTIONS) .args(STDIN_BASE_OPTIONS)
.arg("--show-settings") .arg("--show-settings")
.arg("test.py") .arg("test.py")
.current_dir(&project_dir), @r###" .current_dir(&project_dir), @r#"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
@ -4535,6 +4543,7 @@ from typing import Union;foo: Union[int, str] = 1
XXX, XXX,
] ]
linter.typing_modules = [] linter.typing_modules = []
linter.typing_extensions = true
# Linter Plugins # Linter Plugins
linter.flake8_annotations.mypy_init_return = false linter.flake8_annotations.mypy_init_return = false
@ -4718,7 +4727,7 @@ from typing import Union;foo: Union[int, str] = 1
analyze.include_dependencies = {} analyze.include_dependencies = {}
----- stderr ----- ----- stderr -----
"###); "#);
}); });
Ok(()) Ok(())

View file

@ -213,6 +213,7 @@ linter.task_tags = [
XXX, XXX,
] ]
linter.typing_modules = [] linter.typing_modules = []
linter.typing_extensions = true
# Linter Plugins # Linter Plugins
linter.flake8_annotations.mypy_init_return = false linter.flake8_annotations.mypy_init_return = false

View file

@ -534,26 +534,50 @@ impl<'a> Checker<'a> {
self.semantic_checker = checker; 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`, `member` is imported from `typing_extensions`, while
/// on Python >=`version_added_to_typing`, it is imported from `typing`. /// on Python >=`version_added_to_typing`, it is imported from `typing`.
/// ///
/// See [`Importer::get_or_import_symbol`] for more details on the returned values. /// If the Python version is less than `version_added_to_typing` but
pub(crate) fn import_from_typing( /// `LinterSettings::typing_extensions` is `false`, this method returns `None`.
&self, pub(crate) fn typing_importer<'b>(
member: &str, &'b self,
position: TextSize, member: &'b str,
version_added_to_typing: PythonVersion, version_added_to_typing: PythonVersion,
) -> Result<(Edit, String), ResolutionError> { ) -> Option<TypingImporter<'b, 'a>> {
let source_module = if self.target_version() >= version_added_to_typing { let source_module = if self.target_version() >= version_added_to_typing {
"typing" "typing"
} else if !self.settings.typing_extensions {
return None;
} else { } else {
"typing_extensions" "typing_extensions"
}; };
let request = ImportRequest::import_from(source_module, member); Some(TypingImporter {
self.importer() checker: self,
.get_or_import_symbol(&request, position, self.semantic()) 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())
} }
} }

View file

@ -791,8 +791,9 @@ mod tests {
use crate::linter::check_path; use crate::linter::check_path;
use crate::message::Message; use crate::message::Message;
use crate::registry::Rule; use crate::registry::Rule;
use crate::settings::LinterSettings;
use crate::source_kind::SourceKind; 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}; use crate::{assert_messages, directives, settings, Locator};
/// Construct a path to a Jupyter notebook in the `resources/test/fixtures/jupyter` directory. /// Construct a path to a Jupyter notebook in the `resources/test/fixtures/jupyter` directory.
@ -811,7 +812,7 @@ mod tests {
} = assert_notebook_path( } = assert_notebook_path(
&actual, &actual,
expected, expected,
&settings::LinterSettings::for_rule(Rule::UnsortedImports), &LinterSettings::for_rule(Rule::UnsortedImports),
)?; )?;
assert_messages!(messages, actual, source_notebook); assert_messages!(messages, actual, source_notebook);
Ok(()) Ok(())
@ -828,7 +829,7 @@ mod tests {
} = assert_notebook_path( } = assert_notebook_path(
&actual, &actual,
expected, expected,
&settings::LinterSettings::for_rule(Rule::UnusedImport), &LinterSettings::for_rule(Rule::UnusedImport),
)?; )?;
assert_messages!(messages, actual, source_notebook); assert_messages!(messages, actual, source_notebook);
Ok(()) Ok(())
@ -845,7 +846,7 @@ mod tests {
} = assert_notebook_path( } = assert_notebook_path(
&actual, &actual,
expected, expected,
&settings::LinterSettings::for_rule(Rule::UnusedVariable), &LinterSettings::for_rule(Rule::UnusedVariable),
)?; )?;
assert_messages!(messages, actual, source_notebook); assert_messages!(messages, actual, source_notebook);
Ok(()) Ok(())
@ -862,7 +863,7 @@ mod tests {
} = assert_notebook_path( } = assert_notebook_path(
&actual, &actual,
expected, expected,
&settings::LinterSettings::for_rule(Rule::UndefinedName), &LinterSettings::for_rule(Rule::UndefinedName),
)?; )?;
assert_messages!(messages, actual, source_notebook); assert_messages!(messages, actual, source_notebook);
Ok(()) Ok(())
@ -879,7 +880,7 @@ mod tests {
} = assert_notebook_path( } = assert_notebook_path(
actual_path, actual_path,
&expected_path, &expected_path,
&settings::LinterSettings::for_rule(Rule::UnusedImport), &LinterSettings::for_rule(Rule::UnusedImport),
)?; )?;
let mut writer = Vec::new(); let mut writer = Vec::new();
fixed_notebook.write(&mut writer)?; fixed_notebook.write(&mut writer)?;
@ -900,7 +901,7 @@ mod tests {
} = assert_notebook_path( } = assert_notebook_path(
&actual, &actual,
expected, expected,
&settings::LinterSettings::for_rule(Rule::UnusedImport), &LinterSettings::for_rule(Rule::UnusedImport),
)?; )?;
assert_messages!(messages, actual, source_notebook); assert_messages!(messages, actual, source_notebook);
Ok(()) Ok(())
@ -930,7 +931,7 @@ mod tests {
let (_, transformed) = test_contents( let (_, transformed) = test_contents(
&source_kind, &source_kind,
path, path,
&settings::LinterSettings::for_rule(Rule::UnusedImport), &LinterSettings::for_rule(Rule::UnusedImport),
); );
let linted_notebook = transformed.into_owned().expect_ipy_notebook(); let linted_notebook = transformed.into_owned().expect_ipy_notebook();
let mut writer = Vec::new(); 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 /// Wrapper around `test_contents_syntax_errors` for testing a snippet of code instead of a
/// file. /// file.
fn test_snippet_syntax_errors( fn test_snippet_syntax_errors(contents: &str, settings: &LinterSettings) -> Vec<Message> {
contents: &str,
settings: &settings::LinterSettings,
) -> Vec<Message> {
let contents = dedent(contents); let contents = dedent(contents);
test_contents_syntax_errors( test_contents_syntax_errors(
&SourceKind::Python(contents.to_string()), &SourceKind::Python(contents.to_string()),
@ -963,7 +961,7 @@ mod tests {
fn test_contents_syntax_errors( fn test_contents_syntax_errors(
source_kind: &SourceKind, source_kind: &SourceKind,
path: &Path, path: &Path,
settings: &settings::LinterSettings, settings: &LinterSettings,
) -> Vec<Message> { ) -> Vec<Message> {
let source_type = PySourceType::from(path); let source_type = PySourceType::from(path);
let options = let options =
@ -1032,7 +1030,7 @@ mod tests {
let snapshot = format!("async_comprehension_in_sync_comprehension_{name}_{python_version}"); let snapshot = format!("async_comprehension_in_sync_comprehension_{name}_{python_version}");
let messages = test_snippet_syntax_errors( let messages = test_snippet_syntax_errors(
contents, contents,
&settings::LinterSettings { &LinterSettings {
rules: settings::rule_table::RuleTable::empty(), rules: settings::rule_table::RuleTable::empty(),
unresolved_target_version: python_version, unresolved_target_version: python_version,
preview: settings::types::PreviewMode::Enabled, preview: settings::types::PreviewMode::Enabled,
@ -1051,7 +1049,7 @@ mod tests {
let messages = test_contents_syntax_errors( let messages = test_contents_syntax_errors(
&SourceKind::IpyNotebook(Notebook::from_path(path)?), &SourceKind::IpyNotebook(Notebook::from_path(path)?),
path, path,
&settings::LinterSettings { &LinterSettings {
unresolved_target_version: python_version, unresolved_target_version: python_version,
rules: settings::rule_table::RuleTable::empty(), rules: settings::rule_table::RuleTable::empty(),
preview: settings::types::PreviewMode::Enabled, preview: settings::types::PreviewMode::Enabled,
@ -1076,7 +1074,7 @@ mod tests {
let messages = test_contents_syntax_errors( let messages = test_contents_syntax_errors(
&SourceKind::Python(std::fs::read_to_string(&path)?), &SourceKind::Python(std::fs::read_to_string(&path)?),
&path, &path,
&settings::LinterSettings::for_rule(rule), &LinterSettings::for_rule(rule),
); );
insta::with_settings!({filters => vec![(r"\\", "/")]}, { insta::with_settings!({filters => vec![(r"\\", "/")]}, {
assert_messages!(snapshot, messages); assert_messages!(snapshot, messages);
@ -1095,10 +1093,108 @@ mod tests {
} = assert_notebook_path( } = assert_notebook_path(
path, path,
path, path,
&settings::LinterSettings::for_rule(Rule::YieldOutsideFunction), &LinterSettings::for_rule(Rule::YieldOutsideFunction),
)?; )?;
assert_messages!(messages, path, source_notebook); assert_messages!(messages, path, source_notebook);
Ok(()) 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("<filename>.pyi");
let contents = dedent(contents);
let messages = test_contents(&SourceKind::Python(contents.into_owned()), path, settings).0;
assert_messages!(snapshot, messages);
}
} }

View file

@ -59,6 +59,16 @@ use ruff_python_ast::PythonVersion;
/// return commons /// 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 /// [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-annotated]: https://docs.python.org/3/library/typing.html#typing.Annotated
/// [typing-extensions]: https://typing-extensions.readthedocs.io/en/stable/ /// [typing-extensions]: https://typing-extensions.readthedocs.io/en/stable/
@ -223,6 +233,10 @@ fn create_diagnostic(
dependency_call: Option<DependencyCall>, dependency_call: Option<DependencyCall>,
mut seen_default: bool, mut seen_default: bool,
) -> bool { ) -> bool {
let Some(importer) = checker.typing_importer("Annotated", PythonVersion::PY39) else {
return seen_default;
};
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
FastApiNonAnnotatedDependency { FastApiNonAnnotatedDependency {
py_version: checker.target_version(), py_version: checker.target_version(),
@ -231,11 +245,7 @@ fn create_diagnostic(
); );
let try_generate_fix = || { let try_generate_fix = || {
let (import_edit, binding) = checker.import_from_typing( let (import_edit, binding) = importer.import(parameter.range.start())?;
"Annotated",
parameter.range.start(),
PythonVersion::PY39,
)?;
// Each of these classes takes a single, optional default // Each of these classes takes a single, optional default
// argument, followed by kw-only arguments // argument, followed by kw-only arguments

View file

@ -131,7 +131,8 @@ impl AutoPythonType {
"NoReturn" "NoReturn"
}; };
let (no_return_edit, binding) = checker let (no_return_edit, binding) = checker
.import_from_typing(member, at, PythonVersion::lowest()) .typing_importer(member, PythonVersion::lowest())?
.import(at)
.ok()?; .ok()?;
let expr = Expr::Name(ast::ExprName { let expr = Expr::Name(ast::ExprName {
id: Name::from(binding), id: Name::from(binding),
@ -169,7 +170,8 @@ impl AutoPythonType {
// Ex) `Optional[int]` // Ex) `Optional[int]`
let (optional_edit, binding) = checker let (optional_edit, binding) = checker
.import_from_typing("Optional", at, PythonVersion::lowest()) .typing_importer("Optional", PythonVersion::lowest())?
.import(at)
.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]))
@ -182,7 +184,8 @@ impl AutoPythonType {
// Ex) `Union[int, str]` // Ex) `Union[int, str]`
let (union_edit, binding) = checker let (union_edit, binding) = checker
.import_from_typing("Union", at, PythonVersion::lowest()) .typing_importer("Union", PythonVersion::lowest())?
.import(at)
.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

@ -224,6 +224,16 @@ impl Violation for MissingTypeCls {
/// def add(a: int, b: int) -> int: /// def add(a: int, b: int) -> int:
/// return a + b /// 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)] #[derive(ViolationMetadata)]
pub(crate) struct MissingReturnTypeUndocumentedPublicFunction { pub(crate) struct MissingReturnTypeUndocumentedPublicFunction {
name: String, name: String,
@ -267,6 +277,16 @@ impl Violation for MissingReturnTypeUndocumentedPublicFunction {
/// def _add(a: int, b: int) -> int: /// def _add(a: int, b: int) -> int:
/// return a + b /// 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)] #[derive(ViolationMetadata)]
pub(crate) struct MissingReturnTypePrivateFunction { pub(crate) struct MissingReturnTypePrivateFunction {
name: String, name: String,

View file

@ -10,7 +10,7 @@ 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}; use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker; use crate::checkers::ast::{Checker, TypingImporter};
use ruff_python_ast::PythonVersion; use ruff_python_ast::PythonVersion;
/// ## What it does /// ## 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 /// The fix is only marked as unsafe if there is the possibility that it might delete a comment
/// from your code. /// 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 673]: https://peps.python.org/pep-0673/#motivation
/// [PEP-695]: https://peps.python.org/pep-0695/ /// [PEP-695]: https://peps.python.org/pep-0695/
/// [PYI018]: https://docs.astral.sh/ruff/rules/unused-private-type-var/ /// [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 semantic = checker.semantic();
let current_scope = &semantic.scopes[binding.scope]; let current_scope = &semantic.scopes[binding.scope];
let function_def = binding.statement(semantic)?.as_function_def_stmt()?; let function_def = binding.statement(semantic)?.as_function_def_stmt()?;
let importer = checker.typing_importer("Self", PythonVersion::PY311)?;
let ast::StmtFunctionDef { let ast::StmtFunctionDef {
name: function_name, name: function_name,
@ -178,6 +189,7 @@ pub(crate) fn custom_type_var_instead_of_self(
diagnostic.try_set_fix(|| { diagnostic.try_set_fix(|| {
replace_custom_typevar_with_self( replace_custom_typevar_with_self(
checker, checker,
&importer,
function_def, function_def,
custom_typevar, custom_typevar,
self_or_cls_parameter, 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 /// * If it was a PEP-695 type variable, removes that `TypeVar` from the PEP-695 type-parameter list
fn replace_custom_typevar_with_self( fn replace_custom_typevar_with_self(
checker: &Checker, checker: &Checker,
importer: &TypingImporter,
function_def: &ast::StmtFunctionDef, function_def: &ast::StmtFunctionDef,
custom_typevar: TypeVar, custom_typevar: TypeVar,
self_or_cls_parameter: &ast::ParameterWithDefault, self_or_cls_parameter: &ast::ParameterWithDefault,
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) = let (import_edit, self_symbol_binding) = importer.import(function_def.start())?;
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(

View file

@ -1,21 +1,18 @@
use std::collections::HashSet; use std::collections::HashSet;
use anyhow::Result;
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::name::Name; use ruff_python_ast::{Expr, ExprBinOp, Operator, PythonVersion};
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 super::generate_union_fix;
/// ## What it does /// ## What it does
/// Checks for duplicate union members. /// Checks for duplicate union members.
/// ///
@ -118,7 +115,19 @@ pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) {
applicability, applicability,
)), )),
UnionKind::TypingUnion => { 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, 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<Fix> {
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,
))
}

View file

@ -1,5 +1,14 @@
use std::fmt; 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 any_eq_ne_annotation::*;
pub(crate) use bad_generator_return_type::*; pub(crate) use bad_generator_return_type::*;
pub(crate) use bad_version_info_comparison::*; pub(crate) use bad_version_info_comparison::*;
@ -108,3 +117,39 @@ impl fmt::Display for TypingModule {
fmt.write_str(self.as_str()) 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<Fix> {
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,
))
}

View file

@ -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_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;
@ -75,6 +75,16 @@ use ruff_text_size::Ranged;
/// ## Fix safety /// ## Fix safety
/// This rule's fix is marked as unsafe as it changes the meaning of your type annotations. /// 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 /// ## References
/// - [Python documentation: `typing.Self`](https://docs.python.org/3/library/typing.html#typing.Self) /// - [Python documentation: `typing.Self`](https://docs.python.org/3/library/typing.html#typing.Self)
#[derive(ViolationMetadata)] #[derive(ViolationMetadata)]
@ -192,6 +202,10 @@ fn add_diagnostic(
class_def: &ast::StmtClassDef, class_def: &ast::StmtClassDef,
method_name: &str, method_name: &str,
) { ) {
let Some(importer) = checker.typing_importer("Self", PythonVersion::PY311) else {
return;
};
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
NonSelfReturnType { NonSelfReturnType {
class_name: class_def.name.to_string(), class_name: class_def.name.to_string(),
@ -200,21 +214,21 @@ fn add_diagnostic(
stmt.identifier(), 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); checker.report_diagnostic(diagnostic);
} }
fn replace_with_self_fix( fn replace_with_self_fix(
checker: &Checker, semantic: &SemanticModel,
importer: &TypingImporter,
stmt: &ast::Stmt, stmt: &ast::Stmt,
returns: &ast::Expr, returns: &ast::Expr,
class_def: &ast::StmtClassDef, class_def: &ast::StmtClassDef,
) -> anyhow::Result<Fix> { ) -> anyhow::Result<Fix> {
let semantic = checker.semantic(); let (self_import, self_binding) = importer.import(returns.start())?;
let (self_import, self_binding) =
checker.import_from_typing("Self", returns.start(), PythonVersion::PY311)?;
let mut others = Vec::with_capacity(2); let mut others = Vec::with_capacity(2);
@ -230,7 +244,7 @@ fn replace_with_self_fix(
others.extend(remove_first_argument_type_hint()); others.extend(remove_first_argument_type_hint());
others.push(Edit::range_replacement(self_binding, returns.range())); 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 Applicability::DisplayOnly
} else { } else {
Applicability::Unsafe Applicability::Unsafe

View file

@ -222,11 +222,11 @@ fn create_fix(
let fix = match union_kind { let fix = match union_kind {
UnionKind::TypingOptional => { UnionKind::TypingOptional => {
let (import_edit, bound_name) = checker.import_from_typing( let Some(importer) = checker.typing_importer("Optional", PythonVersion::lowest())
"Optional", else {
literal_expr.start(), return Ok(None);
PythonVersion::lowest(), };
)?; let (import_edit, bound_name) = importer.import(literal_expr.start())?;
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);
let optional_edit = Edit::range_replacement(content, literal_expr.range()); let optional_edit = Edit::range_replacement(content, literal_expr.range());

View file

@ -1,18 +1,15 @@
use bitflags::bitflags; use bitflags::bitflags;
use anyhow::Result;
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::{ use ruff_python_ast::{AnyParameterRef, Expr, ExprBinOp, Operator, Parameters, PythonVersion};
name::Name, AnyParameterRef, Expr, ExprBinOp, ExprContext, ExprName, ExprSubscript, ExprTuple,
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; use crate::checkers::ast::Checker;
use super::generate_union_fix;
/// ## What it does /// ## What it does
/// Checks for parameter annotations that contain redundant unions between /// Checks for parameter annotations that contain redundant unions between
/// builtin numeric types (e.g., `int | float`). /// builtin numeric types (e.g., `int | float`).
@ -157,7 +154,18 @@ fn check_annotation<'a>(checker: &Checker, annotation: &'a Expr) {
applicability, applicability,
)), )),
UnionKind::TypingUnion => { 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, 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<Fix> {
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,
))
}

View file

@ -217,6 +217,16 @@ impl Violation for UnassignedSpecialVariableInStub {
/// ///
/// Vector: TypeAlias = list[float] /// 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)] #[derive(ViolationMetadata)]
pub(crate) struct TypeAliasWithoutAnnotation { pub(crate) struct TypeAliasWithoutAnnotation {
module: TypingModule, module: TypingModule,
@ -672,6 +682,10 @@ pub(crate) fn type_alias_without_annotation(checker: &Checker, value: &Expr, tar
TypingModule::TypingExtensions TypingModule::TypingExtensions
}; };
let Some(importer) = checker.typing_importer("TypeAlias", PythonVersion::PY310) else {
return;
};
let mut diagnostic = Diagnostic::new( let mut diagnostic = Diagnostic::new(
TypeAliasWithoutAnnotation { TypeAliasWithoutAnnotation {
module, module,
@ -681,8 +695,7 @@ 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) = let (import_edit, binding) = importer.import(target.start())?;
checker.import_from_typing("TypeAlias", target.start(), PythonVersion::PY310)?;
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

@ -1,6 +1,6 @@
use std::fmt; use std::fmt;
use anyhow::Result; use anyhow::{Context, Result};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation}; use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_macros::{derive_message_formats, ViolationMetadata};
@ -136,8 +136,10 @@ fn generate_fix(checker: &Checker, conversion_type: ConversionType, expr: &Expr)
))) )))
} }
ConversionType::Optional => { ConversionType::Optional => {
let (import_edit, binding) = let importer = checker
checker.import_from_typing("Optional", expr.start(), PythonVersion::lowest())?; .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 { 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

@ -250,6 +250,7 @@ pub struct LinterSettings {
pub line_length: LineLength, pub line_length: LineLength,
pub task_tags: Vec<String>, pub task_tags: Vec<String>,
pub typing_modules: Vec<String>, pub typing_modules: Vec<String>,
pub typing_extensions: bool,
// Plugins // Plugins
pub flake8_annotations: flake8_annotations::settings::Settings, pub flake8_annotations: flake8_annotations::settings::Settings,
@ -313,6 +314,7 @@ impl Display for LinterSettings {
self.line_length, self.line_length,
self.task_tags | array, self.task_tags | array,
self.typing_modules | array, self.typing_modules | array,
self.typing_extensions,
] ]
} }
writeln!(f, "\n# Linter Plugins")?; writeln!(f, "\n# Linter Plugins")?;
@ -450,6 +452,7 @@ impl LinterSettings {
preview: PreviewMode::default(), preview: PreviewMode::default(),
explicit_preview_rules: false, explicit_preview_rules: false,
extension: ExtensionMapping::default(), extension: ExtensionMapping::default(),
typing_extensions: true,
} }
} }

View file

@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/linter.rs
---

View file

@ -0,0 +1,23 @@
---
source: crates/ruff_linter/src/linter.rs
---
<filename>: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

View file

@ -0,0 +1,23 @@
---
source: crates/ruff_linter/src/linter.rs
---
<filename>: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

View file

@ -0,0 +1,23 @@
---
source: crates/ruff_linter/src/linter.rs
---
<filename>: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

View file

@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/linter.rs
---

View file

@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/linter.rs
---

View file

@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/linter.rs
---

View file

@ -430,6 +430,7 @@ impl Configuration {
.ruff .ruff
.map(RuffOptions::into_settings) .map(RuffOptions::into_settings)
.unwrap_or_default(), .unwrap_or_default(),
typing_extensions: lint.typing_extensions.unwrap_or(true),
}, },
formatter, formatter,
@ -633,6 +634,7 @@ pub struct LintConfiguration {
pub logger_objects: Option<Vec<String>>, pub logger_objects: Option<Vec<String>>,
pub task_tags: Option<Vec<String>>, pub task_tags: Option<Vec<String>>,
pub typing_modules: Option<Vec<String>>, pub typing_modules: Option<Vec<String>>,
pub typing_extensions: Option<bool>,
// Plugins // Plugins
pub flake8_annotations: Option<Flake8AnnotationsOptions>, pub flake8_annotations: Option<Flake8AnnotationsOptions>,
@ -746,6 +748,7 @@ impl LintConfiguration {
task_tags: options.common.task_tags, task_tags: options.common.task_tags,
logger_objects: options.common.logger_objects, logger_objects: options.common.logger_objects,
typing_modules: options.common.typing_modules, typing_modules: options.common.typing_modules,
typing_extensions: options.typing_extensions,
// Plugins // Plugins
flake8_annotations: options.common.flake8_annotations, flake8_annotations: options.common.flake8_annotations,
@ -1170,6 +1173,7 @@ impl LintConfiguration {
pylint: self.pylint.combine(config.pylint), pylint: self.pylint.combine(config.pylint),
pyupgrade: self.pyupgrade.combine(config.pyupgrade), pyupgrade: self.pyupgrade.combine(config.pyupgrade),
ruff: self.ruff.combine(config.ruff), ruff: self.ruff.combine(config.ruff),
typing_extensions: self.typing_extensions,
} }
} }
} }

View file

@ -513,6 +513,22 @@ pub struct LintOptions {
"# "#
)] )]
pub preview: Option<bool>, pub preview: Option<bool>,
/// 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<bool>,
} }
/// Newtype wrapper for [`LintCommonOptions`] that allows customizing the JSON schema and omitting the fields from the [`OptionsMetadata`]. /// 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<PydoclintOptions>, pydoclint: Option<PydoclintOptions>,
ruff: Option<RuffOptions>, ruff: Option<RuffOptions>,
preview: Option<bool>, preview: Option<bool>,
typing_extensions: Option<bool>,
} }
impl From<LintOptionsWire> for LintOptions { impl From<LintOptionsWire> for LintOptions {
@ -3930,6 +3947,7 @@ impl From<LintOptionsWire> for LintOptions {
pydoclint, pydoclint,
ruff, ruff,
preview, preview,
typing_extensions,
} = value; } = value;
LintOptions { LintOptions {
@ -3985,6 +4003,7 @@ impl From<LintOptionsWire> for LintOptions {
pydoclint, pydoclint,
ruff, ruff,
preview, preview,
typing_extensions,
} }
} }
} }

7
ruff.schema.json generated
View file

@ -2459,6 +2459,13 @@
"type": "string" "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": { "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.", "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": [ "type": [