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

@ -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<Message> {
fn test_snippet_syntax_errors(contents: &str, settings: &LinterSettings) -> Vec<Message> {
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<Message> {
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("<filename>.pyi");
let contents = dedent(contents);
let messages = test_contents(&SourceKind::Python(contents.into_owned()), path, settings).0;
assert_messages!(snapshot, messages);
}
}