Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add per-file-target-version option #16257

Merged
merged 33 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8f4dacc
add option
ntBre Feb 19, 2025
377b6bd
use compiled patterns for resolving
ntBre Feb 19, 2025
b418e31
use Checker::target_version, LinterSettings::resolve_target_version
ntBre Feb 19, 2025
89ac994
allow resolving `per_file_target_version`s in the formatter
ntBre Feb 19, 2025
9f7e057
resolve target_version once for Checker
ntBre Feb 19, 2025
90f6493
test that the linter respects the per-file version
ntBre Feb 19, 2025
aceca51
handle base and absolute paths like PerFileIgnore
ntBre Feb 19, 2025
f369358
convert to Vec<PerFileVersion> earlier like PerFileIgnore
ntBre Feb 19, 2025
ee992b3
test that the formatter respects the per-file version
ntBre Feb 19, 2025
d9f1c17
delete unused Deref impl
ntBre Feb 19, 2025
3bcf6c3
impl Display for CompiledPerFileVersion and use in lint and format
ntBre Feb 20, 2025
f30a3aa
add error case for linter test
ntBre Feb 20, 2025
4c64314
add other case for formatter test
ntBre Feb 20, 2025
09d2989
pass the whole version to is_allowed_module
ntBre Feb 20, 2025
c1bd3a0
pass target_version down to A005
ntBre Feb 20, 2025
89bee8d
extract PerFile<T> and share negation with PerFileVersion
ntBre Feb 20, 2025
b364a4a
add some docs
ntBre Feb 20, 2025
6cf6ccb
factor out CompiledPerFile<T>
ntBre Feb 20, 2025
1c85afa
add generic List type and factor out uses
ntBre Feb 20, 2025
77653e6
remove unused CompiledPerFile<T> newtypes
ntBre Feb 20, 2025
920df2d
add PerFileKind to fix labels
ntBre Feb 20, 2025
5d02c60
rename to PerFileTargetVersion
ntBre Feb 20, 2025
276f3ab
add context on glob failure
ntBre Feb 20, 2025
699f273
also add context to resolve call itself
ntBre Feb 20, 2025
5f7e8b0
expand docs on `per-file-target-version`
ntBre Feb 20, 2025
1f4dced
rename target_version fields and document them along with per-file
ntBre Feb 20, 2025
d9d6022
pass Option<&Path> to to_format_options
ntBre Feb 20, 2025
778c559
tidy up
ntBre Feb 20, 2025
d4f2bba
fix ci
ntBre Feb 20, 2025
80699a1
mention Checker
ntBre Feb 21, 2025
23f370e
switch to debug_label
ntBre Feb 21, 2025
375fd5c
impl CacheKey manually to avoid constraints on structs
ntBre Feb 21, 2025
34345de
test script formatting in the server
ntBre Feb 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading