Skip to content

Commit 01a31c0

Browse files
authored
Add config option to disable typing_extensions imports (#17611)
Summary -- This PR resolves #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 #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]: #9761 (comment) 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.
1 parent 405878a commit 01a31c0

26 files changed

Lines changed: 470 additions & 160 deletions

crates/ruff/tests/lint.rs

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,7 @@ fn value_given_to_table_key_is_not_inline_table_2() {
994994
- `lint.extend-per-file-ignores`
995995
- `lint.exclude`
996996
- `lint.preview`
997+
- `lint.typing-extensions`
997998
998999
For more information, try '--help'.
9991000
");
@@ -2117,7 +2118,7 @@ requires-python = ">= 3.11"
21172118
.arg("test.py")
21182119
.arg("-")
21192120
.current_dir(project_dir)
2120-
, @r###"
2121+
, @r#"
21212122
success: true
21222123
exit_code: 0
21232124
----- stdout -----
@@ -2207,6 +2208,7 @@ requires-python = ">= 3.11"
22072208
XXX,
22082209
]
22092210
linter.typing_modules = []
2211+
linter.typing_extensions = true
22102212
22112213
# Linter Plugins
22122214
linter.flake8_annotations.mypy_init_return = false
@@ -2390,7 +2392,7 @@ requires-python = ">= 3.11"
23902392
analyze.include_dependencies = {}
23912393
23922394
----- stderr -----
2393-
"###);
2395+
"#);
23942396
});
23952397
Ok(())
23962398
}
@@ -2428,7 +2430,7 @@ requires-python = ">= 3.11"
24282430
.arg("test.py")
24292431
.arg("-")
24302432
.current_dir(project_dir)
2431-
, @r###"
2433+
, @r#"
24322434
success: true
24332435
exit_code: 0
24342436
----- stdout -----
@@ -2518,6 +2520,7 @@ requires-python = ">= 3.11"
25182520
XXX,
25192521
]
25202522
linter.typing_modules = []
2523+
linter.typing_extensions = true
25212524
25222525
# Linter Plugins
25232526
linter.flake8_annotations.mypy_init_return = false
@@ -2701,7 +2704,7 @@ requires-python = ">= 3.11"
27012704
analyze.include_dependencies = {}
27022705
27032706
----- stderr -----
2704-
"###);
2707+
"#);
27052708
});
27062709
Ok(())
27072710
}
@@ -2790,7 +2793,7 @@ from typing import Union;foo: Union[int, str] = 1
27902793
.args(STDIN_BASE_OPTIONS)
27912794
.arg("test.py")
27922795
.arg("--show-settings")
2793-
.current_dir(project_dir), @r###"
2796+
.current_dir(project_dir), @r#"
27942797
success: true
27952798
exit_code: 0
27962799
----- stdout -----
@@ -2881,6 +2884,7 @@ from typing import Union;foo: Union[int, str] = 1
28812884
XXX,
28822885
]
28832886
linter.typing_modules = []
2887+
linter.typing_extensions = true
28842888
28852889
# Linter Plugins
28862890
linter.flake8_annotations.mypy_init_return = false
@@ -3064,7 +3068,7 @@ from typing import Union;foo: Union[int, str] = 1
30643068
analyze.include_dependencies = {}
30653069
30663070
----- stderr -----
3067-
"###);
3071+
"#);
30683072
});
30693073
Ok(())
30703074
}
@@ -3170,7 +3174,7 @@ from typing import Union;foo: Union[int, str] = 1
31703174
.arg("--show-settings")
31713175
.args(["--select","UP007"])
31723176
.arg("foo/test.py")
3173-
.current_dir(&project_dir), @r###"
3177+
.current_dir(&project_dir), @r#"
31743178
success: true
31753179
exit_code: 0
31763180
----- stdout -----
@@ -3260,6 +3264,7 @@ from typing import Union;foo: Union[int, str] = 1
32603264
XXX,
32613265
]
32623266
linter.typing_modules = []
3267+
linter.typing_extensions = true
32633268
32643269
# Linter Plugins
32653270
linter.flake8_annotations.mypy_init_return = false
@@ -3443,7 +3448,7 @@ from typing import Union;foo: Union[int, str] = 1
34433448
analyze.include_dependencies = {}
34443449
34453450
----- stderr -----
3446-
"###);
3451+
"#);
34473452
});
34483453
Ok(())
34493454
}
@@ -3497,7 +3502,7 @@ from typing import Union;foo: Union[int, str] = 1
34973502
.arg("--show-settings")
34983503
.args(["--select","UP007"])
34993504
.arg("foo/test.py")
3500-
.current_dir(&project_dir), @r###"
3505+
.current_dir(&project_dir), @r#"
35013506
success: true
35023507
exit_code: 0
35033508
----- stdout -----
@@ -3587,6 +3592,7 @@ from typing import Union;foo: Union[int, str] = 1
35873592
XXX,
35883593
]
35893594
linter.typing_modules = []
3595+
linter.typing_extensions = true
35903596
35913597
# Linter Plugins
35923598
linter.flake8_annotations.mypy_init_return = false
@@ -3770,7 +3776,7 @@ from typing import Union;foo: Union[int, str] = 1
37703776
analyze.include_dependencies = {}
37713777
37723778
----- stderr -----
3773-
"###);
3779+
"#);
37743780
});
37753781
Ok(())
37763782
}
@@ -3823,7 +3829,7 @@ from typing import Union;foo: Union[int, str] = 1
38233829
.args(STDIN_BASE_OPTIONS)
38243830
.arg("--show-settings")
38253831
.arg("foo/test.py")
3826-
.current_dir(&project_dir), @r###"
3832+
.current_dir(&project_dir), @r#"
38273833
success: true
38283834
exit_code: 0
38293835
----- stdout -----
@@ -3914,6 +3920,7 @@ from typing import Union;foo: Union[int, str] = 1
39143920
XXX,
39153921
]
39163922
linter.typing_modules = []
3923+
linter.typing_extensions = true
39173924
39183925
# Linter Plugins
39193926
linter.flake8_annotations.mypy_init_return = false
@@ -4097,7 +4104,7 @@ from typing import Union;foo: Union[int, str] = 1
40974104
analyze.include_dependencies = {}
40984105
40994106
----- stderr -----
4100-
"###);
4107+
"#);
41014108
});
41024109

41034110
insta::with_settings!({
@@ -4107,7 +4114,7 @@ from typing import Union;foo: Union[int, str] = 1
41074114
.args(STDIN_BASE_OPTIONS)
41084115
.arg("--show-settings")
41094116
.arg("test.py")
4110-
.current_dir(project_dir.join("foo")), @r###"
4117+
.current_dir(project_dir.join("foo")), @r#"
41114118
success: true
41124119
exit_code: 0
41134120
----- stdout -----
@@ -4198,6 +4205,7 @@ from typing import Union;foo: Union[int, str] = 1
41984205
XXX,
41994206
]
42004207
linter.typing_modules = []
4208+
linter.typing_extensions = true
42014209
42024210
# Linter Plugins
42034211
linter.flake8_annotations.mypy_init_return = false
@@ -4381,7 +4389,7 @@ from typing import Union;foo: Union[int, str] = 1
43814389
analyze.include_dependencies = {}
43824390
43834391
----- stderr -----
4384-
"###);
4392+
"#);
43854393
});
43864394
Ok(())
43874395
}
@@ -4444,7 +4452,7 @@ from typing import Union;foo: Union[int, str] = 1
44444452
.args(STDIN_BASE_OPTIONS)
44454453
.arg("--show-settings")
44464454
.arg("test.py")
4447-
.current_dir(&project_dir), @r###"
4455+
.current_dir(&project_dir), @r#"
44484456
success: true
44494457
exit_code: 0
44504458
----- stdout -----
@@ -4535,6 +4543,7 @@ from typing import Union;foo: Union[int, str] = 1
45354543
XXX,
45364544
]
45374545
linter.typing_modules = []
4546+
linter.typing_extensions = true
45384547
45394548
# Linter Plugins
45404549
linter.flake8_annotations.mypy_init_return = false
@@ -4718,7 +4727,7 @@ from typing import Union;foo: Union[int, str] = 1
47184727
analyze.include_dependencies = {}
47194728
47204729
----- stderr -----
4721-
"###);
4730+
"#);
47224731
});
47234732

47244733
Ok(())

crates/ruff/tests/snapshots/show_settings__display_default_settings.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ linter.task_tags = [
213213
XXX,
214214
]
215215
linter.typing_modules = []
216+
linter.typing_extensions = true
216217

217218
# Linter Plugins
218219
linter.flake8_annotations.mypy_init_return = false

crates/ruff_linter/src/checkers/ast/mod.rs

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -534,26 +534,50 @@ impl<'a> Checker<'a> {
534534
self.semantic_checker = checker;
535535
}
536536

537-
/// Attempt to create an [`Edit`] that imports `member`.
537+
/// Create a [`TypingImporter`] that will import `member` from either `typing` or
538+
/// `typing_extensions`.
538539
///
539540
/// On Python <`version_added_to_typing`, `member` is imported from `typing_extensions`, while
540541
/// on Python >=`version_added_to_typing`, it is imported from `typing`.
541542
///
542-
/// See [`Importer::get_or_import_symbol`] for more details on the returned values.
543-
pub(crate) fn import_from_typing(
544-
&self,
545-
member: &str,
546-
position: TextSize,
543+
/// If the Python version is less than `version_added_to_typing` but
544+
/// `LinterSettings::typing_extensions` is `false`, this method returns `None`.
545+
pub(crate) fn typing_importer<'b>(
546+
&'b self,
547+
member: &'b str,
547548
version_added_to_typing: PythonVersion,
548-
) -> Result<(Edit, String), ResolutionError> {
549+
) -> Option<TypingImporter<'b, 'a>> {
549550
let source_module = if self.target_version() >= version_added_to_typing {
550551
"typing"
552+
} else if !self.settings.typing_extensions {
553+
return None;
551554
} else {
552555
"typing_extensions"
553556
};
554-
let request = ImportRequest::import_from(source_module, member);
555-
self.importer()
556-
.get_or_import_symbol(&request, position, self.semantic())
557+
Some(TypingImporter {
558+
checker: self,
559+
source_module,
560+
member,
561+
})
562+
}
563+
}
564+
565+
pub(crate) struct TypingImporter<'a, 'b> {
566+
checker: &'a Checker<'b>,
567+
source_module: &'static str,
568+
member: &'a str,
569+
}
570+
571+
impl TypingImporter<'_, '_> {
572+
/// Create an [`Edit`] that makes the requested symbol available at `position`.
573+
///
574+
/// See [`Importer::get_or_import_symbol`] for more details on the returned values and
575+
/// [`Checker::typing_importer`] for a way to construct a [`TypingImporter`].
576+
pub(crate) fn import(&self, position: TextSize) -> Result<(Edit, String), ResolutionError> {
577+
let request = ImportRequest::import_from(self.source_module, self.member);
578+
self.checker
579+
.importer
580+
.get_or_import_symbol(&request, position, self.checker.semantic())
557581
}
558582
}
559583

0 commit comments

Comments
 (0)