Skip to content

Commit

Permalink
Add per-file-target-version option (#16257)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
ntBre authored Feb 24, 2025
1 parent 42a5f5e commit e7a6c19
Show file tree
Hide file tree
Showing 78 changed files with 812 additions and 266 deletions.
4 changes: 2 additions & 2 deletions crates/ruff/src/commands/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ pub(crate) fn format_source(
) -> Result<FormattedSource, FormatCommandError> {
match &source_kind {
SourceKind::Python(unformatted) => {
let options = settings.to_format_options(source_type, unformatted);
let options = settings.to_format_options(source_type, unformatted, path);

let formatted = if let Some(range) = range {
let line_index = LineIndex::from_source_text(unformatted);
Expand Down Expand Up @@ -391,7 +391,7 @@ pub(crate) fn format_source(
));
}

let options = settings.to_format_options(source_type, notebook.source_code());
let options = settings.to_format_options(source_type, notebook.source_code(), path);

let mut output: Option<String> = None;
let mut last: Option<TextSize> = None;
Expand Down
47 changes: 47 additions & 0 deletions crates/ruff/tests/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2086,3 +2086,50 @@ fn range_formatting_notebook() {
error: Failed to format main.ipynb: Range formatting isn't supported for notebooks.
");
}

/// Test that the formatter respects `per-file-target-version`. Context managers can't be
/// parenthesized like this before Python 3.10.
///
/// Adapted from <https://github.com/python/cpython/issues/56991#issuecomment-1093555135>
#[test]
fn per_file_target_version_formatter() {
// without `per-file-target-version` this should not be reformatted in the same way
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--isolated", "--stdin-filename", "test.py", "--target-version=py38"])
.arg("-")
.pass_stdin(r#"
with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a_really_long_baz") as baz:
pass
"#), @r#"
success: true
exit_code: 0
----- stdout -----
with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open(
"a_really_long_baz"
) as baz:
pass
----- stderr -----
"#);

assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--isolated", "--stdin-filename", "test.py", "--target-version=py38"])
.args(["--config", r#"per-file-target-version = {"test.py" = "py311"}"#])
.arg("-")
.pass_stdin(r#"
with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a_really_long_baz") as baz:
pass
"#), @r#"
success: true
exit_code: 0
----- stdout -----
with (
open("a_really_long_foo") as foo,
open("a_really_long_bar") as bar,
open("a_really_long_baz") as baz,
):
pass
----- stderr -----
"#);
}
60 changes: 60 additions & 0 deletions crates/ruff/tests/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2567,3 +2567,63 @@ fn a005_module_shadowing_strict_default() -> Result<()> {
});
Ok(())
}

/// Test that the linter respects per-file-target-version.
#[test]
fn per_file_target_version_linter() {
// without per-file-target-version, there should be one UP046 error
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--target-version", "py312"])
.args(["--select", "UP046"]) // only triggers on 3.12+
.args(["--stdin-filename", "test.py"])
.arg("--preview")
.arg("-")
.pass_stdin(r#"
from typing import Generic, TypeVar
T = TypeVar("T")
class A(Generic[T]):
var: T
"#),
@r"
success: false
exit_code: 1
----- stdout -----
test.py:6:9: UP046 Generic class `A` uses `Generic` subclass instead of type parameters
Found 1 error.
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr -----
"
);

// with per-file-target-version, there should be no errors because the new generic syntax is
// unavailable
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--target-version", "py312"])
.args(["--config", r#"per-file-target-version = {"test.py" = "py311"}"#])
.args(["--select", "UP046"]) // only triggers on 3.12+
.args(["--stdin-filename", "test.py"])
.arg("--preview")
.arg("-")
.pass_stdin(r#"
from typing import Generic, TypeVar
T = TypeVar("T")
class A(Generic[T]):
var: T
"#),
@r"
success: true
exit_code: 0
----- stdout -----
All checks passed!
----- stderr -----
"
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ linter.rules.should_fix = [
linter.per_file_ignores = {}
linter.safety_table.forced_safe = []
linter.safety_table.forced_unsafe = []
linter.target_version = 3.7
linter.unresolved_target_version = 3.7
linter.per_file_target_version = {}
linter.preview = disabled
linter.explicit_preview_rules = false
linter.extension = ExtensionMapping({})
Expand Down Expand Up @@ -373,7 +374,8 @@ linter.ruff.allowed_markup_calls = []

# Formatter Settings
formatter.exclude = []
formatter.target_version = 3.7
formatter.unresolved_target_version = 3.7
formatter.per_file_target_version = {}
formatter.preview = disabled
formatter.line_width = 100
formatter.line_ending = auto
Expand Down
40 changes: 20 additions & 20 deletions crates/ruff_linter/src/checkers/ast/analyze/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
{
if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
if !checker.semantic.future_annotations_or_stub()
&& checker.settings.target_version < PythonVersion::PY310
&& checker.settings.target_version >= PythonVersion::PY37
&& checker.target_version() < PythonVersion::PY310
&& checker.target_version() >= PythonVersion::PY37
&& checker.semantic.in_annotation()
&& !checker.settings.pyupgrade.keep_runtime_typing
{
Expand All @@ -49,8 +49,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
Rule::NonPEP604AnnotationOptional,
]) {
if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::PY310
|| (checker.settings.target_version >= PythonVersion::PY37
|| checker.target_version() >= PythonVersion::PY310
|| (checker.target_version() >= PythonVersion::PY37
&& checker.semantic.future_annotations_or_stub()
&& checker.semantic.in_annotation()
&& !checker.settings.pyupgrade.keep_runtime_typing)
Expand All @@ -64,7 +64,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
// Ex) list[...]
if checker.enabled(Rule::FutureRequiredTypeAnnotation) {
if !checker.semantic.future_annotations_or_stub()
&& checker.settings.target_version < PythonVersion::PY39
&& checker.target_version() < PythonVersion::PY39
&& checker.semantic.in_annotation()
&& checker.semantic.in_runtime_evaluated_annotation()
&& !checker.semantic.in_string_type_definition()
Expand Down Expand Up @@ -135,7 +135,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
}

if checker.enabled(Rule::UnnecessaryDefaultTypeArgs) {
if checker.settings.target_version >= PythonVersion::PY313 {
if checker.target_version() >= PythonVersion::PY313 {
pyupgrade::rules::unnecessary_default_type_args(checker, expr);
}
}
Expand Down Expand Up @@ -268,8 +268,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
{
if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
if !checker.semantic.future_annotations_or_stub()
&& checker.settings.target_version < PythonVersion::PY39
&& checker.settings.target_version >= PythonVersion::PY37
&& checker.target_version() < PythonVersion::PY39
&& checker.target_version() >= PythonVersion::PY37
&& checker.semantic.in_annotation()
&& !checker.settings.pyupgrade.keep_runtime_typing
{
Expand All @@ -278,8 +278,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
}
if checker.enabled(Rule::NonPEP585Annotation) {
if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::PY39
|| (checker.settings.target_version >= PythonVersion::PY37
|| checker.target_version() >= PythonVersion::PY39
|| (checker.target_version() >= PythonVersion::PY37
&& checker.semantic.future_annotations_or_stub()
&& checker.semantic.in_annotation()
&& !checker.settings.pyupgrade.keep_runtime_typing)
Expand Down Expand Up @@ -378,8 +378,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if let Some(replacement) = typing::to_pep585_generic(expr, &checker.semantic) {
if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
if !checker.semantic.future_annotations_or_stub()
&& checker.settings.target_version < PythonVersion::PY39
&& checker.settings.target_version >= PythonVersion::PY37
&& checker.target_version() < PythonVersion::PY39
&& checker.target_version() >= PythonVersion::PY37
&& checker.semantic.in_annotation()
&& !checker.settings.pyupgrade.keep_runtime_typing
{
Expand All @@ -390,8 +390,8 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
}
if checker.enabled(Rule::NonPEP585Annotation) {
if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::PY39
|| (checker.settings.target_version >= PythonVersion::PY37
|| checker.target_version() >= PythonVersion::PY39
|| (checker.target_version() >= PythonVersion::PY37
&& checker.semantic.future_annotations_or_stub()
&& checker.semantic.in_annotation()
&& !checker.settings.pyupgrade.keep_runtime_typing)
Expand All @@ -405,7 +405,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
refurb::rules::regex_flag_alias(checker, expr);
}
if checker.enabled(Rule::DatetimeTimezoneUTC) {
if checker.settings.target_version >= PythonVersion::PY311 {
if checker.target_version() >= PythonVersion::PY311 {
pyupgrade::rules::datetime_utc_alias(checker, expr);
}
}
Expand Down Expand Up @@ -610,12 +610,12 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
pyupgrade::rules::os_error_alias_call(checker, func);
}
if checker.enabled(Rule::TimeoutErrorAlias) {
if checker.settings.target_version >= PythonVersion::PY310 {
if checker.target_version() >= PythonVersion::PY310 {
pyupgrade::rules::timeout_error_alias_call(checker, func);
}
}
if checker.enabled(Rule::NonPEP604Isinstance) {
if checker.settings.target_version >= PythonVersion::PY310 {
if checker.target_version() >= PythonVersion::PY310 {
pyupgrade::rules::use_pep604_isinstance(checker, expr, func, args);
}
}
Expand Down Expand Up @@ -690,7 +690,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
);
}
if checker.enabled(Rule::ZipWithoutExplicitStrict) {
if checker.settings.target_version >= PythonVersion::PY310 {
if checker.target_version() >= PythonVersion::PY310 {
flake8_bugbear::rules::zip_without_explicit_strict(checker, call);
}
}
Expand Down Expand Up @@ -963,7 +963,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
flake8_pytest_style::rules::fail_call(checker, call);
}
if checker.enabled(Rule::ZipInsteadOfPairwise) {
if checker.settings.target_version >= PythonVersion::PY310 {
if checker.target_version() >= PythonVersion::PY310 {
ruff::rules::zip_instead_of_pairwise(checker, call);
}
}
Expand Down Expand Up @@ -1385,7 +1385,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
// Ex) `str | None`
if checker.enabled(Rule::FutureRequiredTypeAnnotation) {
if !checker.semantic.future_annotations_or_stub()
&& checker.settings.target_version < PythonVersion::PY310
&& checker.target_version() < PythonVersion::PY310
&& checker.semantic.in_annotation()
&& checker.semantic.in_runtime_evaluated_annotation()
&& !checker.semantic.in_string_type_definition()
Expand Down
18 changes: 8 additions & 10 deletions crates/ruff_linter/src/checkers/ast/analyze/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_pyi::rules::str_or_repr_defined_in_stub(checker, stmt);
}
}
if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::PY311
{
if checker.source_type.is_stub() || checker.target_version() >= PythonVersion::PY311 {
if checker.enabled(Rule::NoReturnArgumentAnnotationInStub) {
flake8_pyi::rules::no_return_argument_annotation(checker, parameters);
}
Expand Down Expand Up @@ -194,12 +192,12 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::global_statement(checker, name);
}
if checker.enabled(Rule::LRUCacheWithoutParameters) {
if checker.settings.target_version >= PythonVersion::PY38 {
if checker.target_version() >= PythonVersion::PY38 {
pyupgrade::rules::lru_cache_without_parameters(checker, decorator_list);
}
}
if checker.enabled(Rule::LRUCacheWithMaxsizeNone) {
if checker.settings.target_version >= PythonVersion::PY39 {
if checker.target_version() >= PythonVersion::PY39 {
pyupgrade::rules::lru_cache_with_maxsize_none(checker, decorator_list);
}
}
Expand Down Expand Up @@ -445,7 +443,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pyupgrade::rules::useless_object_inheritance(checker, class_def);
}
if checker.enabled(Rule::ReplaceStrEnum) {
if checker.settings.target_version >= PythonVersion::PY311 {
if checker.target_version() >= PythonVersion::PY311 {
pyupgrade::rules::replace_str_enum(checker, class_def);
}
}
Expand Down Expand Up @@ -765,7 +763,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
if checker.enabled(Rule::UnnecessaryFutureImport) {
if checker.settings.target_version >= PythonVersion::PY37 {
if checker.target_version() >= PythonVersion::PY37 {
if let Some("__future__") = module {
pyupgrade::rules::unnecessary_future_import(checker, stmt, names);
}
Expand Down Expand Up @@ -1039,7 +1037,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
if checker.enabled(Rule::TimeoutErrorAlias) {
if checker.settings.target_version >= PythonVersion::PY310 {
if checker.target_version() >= PythonVersion::PY310 {
if let Some(item) = exc {
pyupgrade::rules::timeout_error_alias_raise(checker, item);
}
Expand Down Expand Up @@ -1431,7 +1429,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_bugbear::rules::jump_statement_in_finally(checker, finalbody);
}
if checker.enabled(Rule::ContinueInFinally) {
if checker.settings.target_version <= PythonVersion::PY38 {
if checker.target_version() <= PythonVersion::PY38 {
pylint::rules::continue_in_finally(checker, finalbody);
}
}
Expand All @@ -1455,7 +1453,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pyupgrade::rules::os_error_alias_handlers(checker, handlers);
}
if checker.enabled(Rule::TimeoutErrorAlias) {
if checker.settings.target_version >= PythonVersion::PY310 {
if checker.target_version() >= PythonVersion::PY310 {
pyupgrade::rules::timeout_error_alias_handlers(checker, handlers);
}
}
Expand Down
Loading

0 comments on commit e7a6c19

Please sign in to comment.