Skip to content

Commit e7a6c19

Browse files
authored
Add per-file-target-version option (#16257)
## Summary This PR is another step in preparing to detect syntax errors in the parser. It introduces the new `per-file-target-version` top-level configuration option, which holds a mapping of compiled glob patterns to Python versions. I intend to use the `LinterSettings::resolve_target_version` method here to pass to the parser: https://github.com/astral-sh/ruff/blob/f50849aeef51a381af6c27df8595ac0e1ef5a891/crates/ruff_linter/src/linter.rs#L491-L493 ## Test Plan I added two new CLI tests to show that the `per-file-target-version` is respected in both the formatter and the linter.
1 parent 42a5f5e commit e7a6c19

File tree

78 files changed

+812
-266
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

78 files changed

+812
-266
lines changed

crates/ruff/src/commands/format.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ pub(crate) fn format_source(
341341
) -> Result<FormattedSource, FormatCommandError> {
342342
match &source_kind {
343343
SourceKind::Python(unformatted) => {
344-
let options = settings.to_format_options(source_type, unformatted);
344+
let options = settings.to_format_options(source_type, unformatted, path);
345345

346346
let formatted = if let Some(range) = range {
347347
let line_index = LineIndex::from_source_text(unformatted);
@@ -391,7 +391,7 @@ pub(crate) fn format_source(
391391
));
392392
}
393393

394-
let options = settings.to_format_options(source_type, notebook.source_code());
394+
let options = settings.to_format_options(source_type, notebook.source_code(), path);
395395

396396
let mut output: Option<String> = None;
397397
let mut last: Option<TextSize> = None;

crates/ruff/tests/format.rs

+47
Original file line numberDiff line numberDiff line change
@@ -2086,3 +2086,50 @@ fn range_formatting_notebook() {
20862086
error: Failed to format main.ipynb: Range formatting isn't supported for notebooks.
20872087
");
20882088
}
2089+
2090+
/// Test that the formatter respects `per-file-target-version`. Context managers can't be
2091+
/// parenthesized like this before Python 3.10.
2092+
///
2093+
/// Adapted from <https://github.com/python/cpython/issues/56991#issuecomment-1093555135>
2094+
#[test]
2095+
fn per_file_target_version_formatter() {
2096+
// without `per-file-target-version` this should not be reformatted in the same way
2097+
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
2098+
.args(["format", "--isolated", "--stdin-filename", "test.py", "--target-version=py38"])
2099+
.arg("-")
2100+
.pass_stdin(r#"
2101+
with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a_really_long_baz") as baz:
2102+
pass
2103+
"#), @r#"
2104+
success: true
2105+
exit_code: 0
2106+
----- stdout -----
2107+
with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open(
2108+
"a_really_long_baz"
2109+
) as baz:
2110+
pass
2111+
2112+
----- stderr -----
2113+
"#);
2114+
2115+
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
2116+
.args(["format", "--isolated", "--stdin-filename", "test.py", "--target-version=py38"])
2117+
.args(["--config", r#"per-file-target-version = {"test.py" = "py311"}"#])
2118+
.arg("-")
2119+
.pass_stdin(r#"
2120+
with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a_really_long_baz") as baz:
2121+
pass
2122+
"#), @r#"
2123+
success: true
2124+
exit_code: 0
2125+
----- stdout -----
2126+
with (
2127+
open("a_really_long_foo") as foo,
2128+
open("a_really_long_bar") as bar,
2129+
open("a_really_long_baz") as baz,
2130+
):
2131+
pass
2132+
2133+
----- stderr -----
2134+
"#);
2135+
}

crates/ruff/tests/lint.rs

+60
Original file line numberDiff line numberDiff line change
@@ -2567,3 +2567,63 @@ fn a005_module_shadowing_strict_default() -> Result<()> {
25672567
});
25682568
Ok(())
25692569
}
2570+
2571+
/// Test that the linter respects per-file-target-version.
2572+
#[test]
2573+
fn per_file_target_version_linter() {
2574+
// without per-file-target-version, there should be one UP046 error
2575+
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
2576+
.args(STDIN_BASE_OPTIONS)
2577+
.args(["--target-version", "py312"])
2578+
.args(["--select", "UP046"]) // only triggers on 3.12+
2579+
.args(["--stdin-filename", "test.py"])
2580+
.arg("--preview")
2581+
.arg("-")
2582+
.pass_stdin(r#"
2583+
from typing import Generic, TypeVar
2584+
2585+
T = TypeVar("T")
2586+
2587+
class A(Generic[T]):
2588+
var: T
2589+
"#),
2590+
@r"
2591+
success: false
2592+
exit_code: 1
2593+
----- stdout -----
2594+
test.py:6:9: UP046 Generic class `A` uses `Generic` subclass instead of type parameters
2595+
Found 1 error.
2596+
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
2597+
2598+
----- stderr -----
2599+
"
2600+
);
2601+
2602+
// with per-file-target-version, there should be no errors because the new generic syntax is
2603+
// unavailable
2604+
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
2605+
.args(STDIN_BASE_OPTIONS)
2606+
.args(["--target-version", "py312"])
2607+
.args(["--config", r#"per-file-target-version = {"test.py" = "py311"}"#])
2608+
.args(["--select", "UP046"]) // only triggers on 3.12+
2609+
.args(["--stdin-filename", "test.py"])
2610+
.arg("--preview")
2611+
.arg("-")
2612+
.pass_stdin(r#"
2613+
from typing import Generic, TypeVar
2614+
2615+
T = TypeVar("T")
2616+
2617+
class A(Generic[T]):
2618+
var: T
2619+
"#),
2620+
@r"
2621+
success: true
2622+
exit_code: 0
2623+
----- stdout -----
2624+
All checks passed!
2625+
2626+
----- stderr -----
2627+
"
2628+
);
2629+
}

crates/ruff/tests/snapshots/show_settings__display_default_settings.snap

+4-2
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,8 @@ linter.rules.should_fix = [
189189
linter.per_file_ignores = {}
190190
linter.safety_table.forced_safe = []
191191
linter.safety_table.forced_unsafe = []
192-
linter.target_version = 3.7
192+
linter.unresolved_target_version = 3.7
193+
linter.per_file_target_version = {}
193194
linter.preview = disabled
194195
linter.explicit_preview_rules = false
195196
linter.extension = ExtensionMapping({})
@@ -373,7 +374,8 @@ linter.ruff.allowed_markup_calls = []
373374

374375
# Formatter Settings
375376
formatter.exclude = []
376-
formatter.target_version = 3.7
377+
formatter.unresolved_target_version = 3.7
378+
formatter.per_file_target_version = {}
377379
formatter.preview = disabled
378380
formatter.line_width = 100
379381
formatter.line_ending = auto

crates/ruff_linter/src/checkers/ast/analyze/expression.rs

+20-20
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
3434
{
3535
if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
3636
if !checker.semantic.future_annotations_or_stub()
37-
&& checker.settings.target_version < PythonVersion::PY310
38-
&& checker.settings.target_version >= PythonVersion::PY37
37+
&& checker.target_version() < PythonVersion::PY310
38+
&& checker.target_version() >= PythonVersion::PY37
3939
&& checker.semantic.in_annotation()
4040
&& !checker.settings.pyupgrade.keep_runtime_typing
4141
{
@@ -49,8 +49,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
4949
Rule::NonPEP604AnnotationOptional,
5050
]) {
5151
if checker.source_type.is_stub()
52-
|| checker.settings.target_version >= PythonVersion::PY310
53-
|| (checker.settings.target_version >= PythonVersion::PY37
52+
|| checker.target_version() >= PythonVersion::PY310
53+
|| (checker.target_version() >= PythonVersion::PY37
5454
&& checker.semantic.future_annotations_or_stub()
5555
&& checker.semantic.in_annotation()
5656
&& !checker.settings.pyupgrade.keep_runtime_typing)
@@ -64,7 +64,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
6464
// Ex) list[...]
6565
if checker.enabled(Rule::FutureRequiredTypeAnnotation) {
6666
if !checker.semantic.future_annotations_or_stub()
67-
&& checker.settings.target_version < PythonVersion::PY39
67+
&& checker.target_version() < PythonVersion::PY39
6868
&& checker.semantic.in_annotation()
6969
&& checker.semantic.in_runtime_evaluated_annotation()
7070
&& !checker.semantic.in_string_type_definition()
@@ -135,7 +135,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
135135
}
136136

137137
if checker.enabled(Rule::UnnecessaryDefaultTypeArgs) {
138-
if checker.settings.target_version >= PythonVersion::PY313 {
138+
if checker.target_version() >= PythonVersion::PY313 {
139139
pyupgrade::rules::unnecessary_default_type_args(checker, expr);
140140
}
141141
}
@@ -268,8 +268,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
268268
{
269269
if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
270270
if !checker.semantic.future_annotations_or_stub()
271-
&& checker.settings.target_version < PythonVersion::PY39
272-
&& checker.settings.target_version >= PythonVersion::PY37
271+
&& checker.target_version() < PythonVersion::PY39
272+
&& checker.target_version() >= PythonVersion::PY37
273273
&& checker.semantic.in_annotation()
274274
&& !checker.settings.pyupgrade.keep_runtime_typing
275275
{
@@ -278,8 +278,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
278278
}
279279
if checker.enabled(Rule::NonPEP585Annotation) {
280280
if checker.source_type.is_stub()
281-
|| checker.settings.target_version >= PythonVersion::PY39
282-
|| (checker.settings.target_version >= PythonVersion::PY37
281+
|| checker.target_version() >= PythonVersion::PY39
282+
|| (checker.target_version() >= PythonVersion::PY37
283283
&& checker.semantic.future_annotations_or_stub()
284284
&& checker.semantic.in_annotation()
285285
&& !checker.settings.pyupgrade.keep_runtime_typing)
@@ -378,8 +378,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
378378
if let Some(replacement) = typing::to_pep585_generic(expr, &checker.semantic) {
379379
if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
380380
if !checker.semantic.future_annotations_or_stub()
381-
&& checker.settings.target_version < PythonVersion::PY39
382-
&& checker.settings.target_version >= PythonVersion::PY37
381+
&& checker.target_version() < PythonVersion::PY39
382+
&& checker.target_version() >= PythonVersion::PY37
383383
&& checker.semantic.in_annotation()
384384
&& !checker.settings.pyupgrade.keep_runtime_typing
385385
{
@@ -390,8 +390,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
390390
}
391391
if checker.enabled(Rule::NonPEP585Annotation) {
392392
if checker.source_type.is_stub()
393-
|| checker.settings.target_version >= PythonVersion::PY39
394-
|| (checker.settings.target_version >= PythonVersion::PY37
393+
|| checker.target_version() >= PythonVersion::PY39
394+
|| (checker.target_version() >= PythonVersion::PY37
395395
&& checker.semantic.future_annotations_or_stub()
396396
&& checker.semantic.in_annotation()
397397
&& !checker.settings.pyupgrade.keep_runtime_typing)
@@ -405,7 +405,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
405405
refurb::rules::regex_flag_alias(checker, expr);
406406
}
407407
if checker.enabled(Rule::DatetimeTimezoneUTC) {
408-
if checker.settings.target_version >= PythonVersion::PY311 {
408+
if checker.target_version() >= PythonVersion::PY311 {
409409
pyupgrade::rules::datetime_utc_alias(checker, expr);
410410
}
411411
}
@@ -610,12 +610,12 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
610610
pyupgrade::rules::os_error_alias_call(checker, func);
611611
}
612612
if checker.enabled(Rule::TimeoutErrorAlias) {
613-
if checker.settings.target_version >= PythonVersion::PY310 {
613+
if checker.target_version() >= PythonVersion::PY310 {
614614
pyupgrade::rules::timeout_error_alias_call(checker, func);
615615
}
616616
}
617617
if checker.enabled(Rule::NonPEP604Isinstance) {
618-
if checker.settings.target_version >= PythonVersion::PY310 {
618+
if checker.target_version() >= PythonVersion::PY310 {
619619
pyupgrade::rules::use_pep604_isinstance(checker, expr, func, args);
620620
}
621621
}
@@ -690,7 +690,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
690690
);
691691
}
692692
if checker.enabled(Rule::ZipWithoutExplicitStrict) {
693-
if checker.settings.target_version >= PythonVersion::PY310 {
693+
if checker.target_version() >= PythonVersion::PY310 {
694694
flake8_bugbear::rules::zip_without_explicit_strict(checker, call);
695695
}
696696
}
@@ -963,7 +963,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
963963
flake8_pytest_style::rules::fail_call(checker, call);
964964
}
965965
if checker.enabled(Rule::ZipInsteadOfPairwise) {
966-
if checker.settings.target_version >= PythonVersion::PY310 {
966+
if checker.target_version() >= PythonVersion::PY310 {
967967
ruff::rules::zip_instead_of_pairwise(checker, call);
968968
}
969969
}
@@ -1385,7 +1385,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
13851385
// Ex) `str | None`
13861386
if checker.enabled(Rule::FutureRequiredTypeAnnotation) {
13871387
if !checker.semantic.future_annotations_or_stub()
1388-
&& checker.settings.target_version < PythonVersion::PY310
1388+
&& checker.target_version() < PythonVersion::PY310
13891389
&& checker.semantic.in_annotation()
13901390
&& checker.semantic.in_runtime_evaluated_annotation()
13911391
&& !checker.semantic.in_string_type_definition()

crates/ruff_linter/src/checkers/ast/analyze/statement.rs

+8-10
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
164164
flake8_pyi::rules::str_or_repr_defined_in_stub(checker, stmt);
165165
}
166166
}
167-
if checker.source_type.is_stub()
168-
|| checker.settings.target_version >= PythonVersion::PY311
169-
{
167+
if checker.source_type.is_stub() || checker.target_version() >= PythonVersion::PY311 {
170168
if checker.enabled(Rule::NoReturnArgumentAnnotationInStub) {
171169
flake8_pyi::rules::no_return_argument_annotation(checker, parameters);
172170
}
@@ -194,12 +192,12 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
194192
pylint::rules::global_statement(checker, name);
195193
}
196194
if checker.enabled(Rule::LRUCacheWithoutParameters) {
197-
if checker.settings.target_version >= PythonVersion::PY38 {
195+
if checker.target_version() >= PythonVersion::PY38 {
198196
pyupgrade::rules::lru_cache_without_parameters(checker, decorator_list);
199197
}
200198
}
201199
if checker.enabled(Rule::LRUCacheWithMaxsizeNone) {
202-
if checker.settings.target_version >= PythonVersion::PY39 {
200+
if checker.target_version() >= PythonVersion::PY39 {
203201
pyupgrade::rules::lru_cache_with_maxsize_none(checker, decorator_list);
204202
}
205203
}
@@ -445,7 +443,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
445443
pyupgrade::rules::useless_object_inheritance(checker, class_def);
446444
}
447445
if checker.enabled(Rule::ReplaceStrEnum) {
448-
if checker.settings.target_version >= PythonVersion::PY311 {
446+
if checker.target_version() >= PythonVersion::PY311 {
449447
pyupgrade::rules::replace_str_enum(checker, class_def);
450448
}
451449
}
@@ -765,7 +763,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
765763
}
766764
}
767765
if checker.enabled(Rule::UnnecessaryFutureImport) {
768-
if checker.settings.target_version >= PythonVersion::PY37 {
766+
if checker.target_version() >= PythonVersion::PY37 {
769767
if let Some("__future__") = module {
770768
pyupgrade::rules::unnecessary_future_import(checker, stmt, names);
771769
}
@@ -1039,7 +1037,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
10391037
}
10401038
}
10411039
if checker.enabled(Rule::TimeoutErrorAlias) {
1042-
if checker.settings.target_version >= PythonVersion::PY310 {
1040+
if checker.target_version() >= PythonVersion::PY310 {
10431041
if let Some(item) = exc {
10441042
pyupgrade::rules::timeout_error_alias_raise(checker, item);
10451043
}
@@ -1431,7 +1429,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
14311429
flake8_bugbear::rules::jump_statement_in_finally(checker, finalbody);
14321430
}
14331431
if checker.enabled(Rule::ContinueInFinally) {
1434-
if checker.settings.target_version <= PythonVersion::PY38 {
1432+
if checker.target_version() <= PythonVersion::PY38 {
14351433
pylint::rules::continue_in_finally(checker, finalbody);
14361434
}
14371435
}
@@ -1455,7 +1453,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
14551453
pyupgrade::rules::os_error_alias_handlers(checker, handlers);
14561454
}
14571455
if checker.enabled(Rule::TimeoutErrorAlias) {
1458-
if checker.settings.target_version >= PythonVersion::PY310 {
1456+
if checker.target_version() >= PythonVersion::PY310 {
14591457
pyupgrade::rules::timeout_error_alias_handlers(checker, handlers);
14601458
}
14611459
}

0 commit comments

Comments
 (0)