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:


f50849aeef/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.
This commit is contained in:
Brent Westbrook 2025-02-24 08:47:13 -05:00 committed by GitHub
parent 42a5f5ef6a
commit e7a6c19e3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
78 changed files with 820 additions and 274 deletions

View file

@ -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);
@ -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;

View file

@ -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 -----
"#);
}

View file

@ -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 -----
"
);
}

View file

@ -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({})
@ -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

View file

@ -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
{
@ -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)
@ -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()
@ -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);
}
}
@ -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
{
@ -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)
@ -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
{
@ -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)
@ -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);
}
}
@ -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);
}
}
@ -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);
}
}
@ -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);
}
}
@ -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()

View file

@ -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);
}
@ -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);
}
}
@ -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);
}
}
@ -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);
}
@ -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);
}
@ -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);
}
}
@ -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);
}
}

View file

@ -38,7 +38,7 @@ use ruff_python_ast::visitor::{walk_except_handler, walk_pattern, Visitor};
use ruff_python_ast::{
self as ast, AnyParameterRef, ArgOrKeyword, Comprehension, ElifElseClause, ExceptHandler, Expr,
ExprContext, FStringElement, Keyword, MatchCase, ModModule, Parameter, Parameters, Pattern,
Stmt, Suite, UnaryOp,
PythonVersion, Stmt, Suite, UnaryOp,
};
use ruff_python_ast::{helpers, str, visitor, PySourceType};
use ruff_python_codegen::{Generator, Stylist};
@ -223,6 +223,8 @@ pub(crate) struct Checker<'a> {
last_stmt_end: TextSize,
/// A state describing if a docstring is expected or not.
docstring_state: DocstringState,
/// The target [`PythonVersion`] for version-dependent checks
target_version: PythonVersion,
}
impl<'a> Checker<'a> {
@ -242,6 +244,7 @@ impl<'a> Checker<'a> {
source_type: PySourceType,
cell_offsets: Option<&'a CellOffsets>,
notebook_index: Option<&'a NotebookIndex>,
target_version: PythonVersion,
) -> Checker<'a> {
let mut semantic = SemanticModel::new(&settings.typing_modules, path, module);
if settings.preview.is_enabled() {
@ -272,6 +275,7 @@ impl<'a> Checker<'a> {
notebook_index,
last_stmt_end: TextSize::default(),
docstring_state: DocstringState::default(),
target_version,
}
}
}
@ -500,6 +504,11 @@ impl<'a> Checker<'a> {
self.report_diagnostic(diagnostic);
}
}
/// Return the [`PythonVersion`] to use for version-related checks.
pub(crate) const fn target_version(&self) -> PythonVersion {
self.target_version
}
}
impl<'a> Visitor<'a> for Checker<'a> {
@ -2108,17 +2117,14 @@ impl<'a> Checker<'a> {
}
fn bind_builtins(&mut self) {
let target_version = self.target_version();
let mut bind_builtin = |builtin| {
// Add the builtin to the scope.
let binding_id = self.semantic.push_builtin();
let scope = self.semantic.global_scope_mut();
scope.add(builtin, binding_id);
};
let standard_builtins = python_builtins(
self.settings.target_version.minor,
self.source_type.is_ipynb(),
);
let standard_builtins = python_builtins(target_version.minor, self.source_type.is_ipynb());
for builtin in standard_builtins {
bind_builtin(builtin);
}
@ -2664,6 +2670,7 @@ pub(crate) fn check_ast(
source_type: PySourceType,
cell_offsets: Option<&CellOffsets>,
notebook_index: Option<&NotebookIndex>,
target_version: PythonVersion,
) -> Vec<Diagnostic> {
let module_path = package
.map(PackageRoot::path)
@ -2703,6 +2710,7 @@ pub(crate) fn check_ast(
source_type,
cell_offsets,
notebook_index,
target_version,
);
checker.bind_builtins();

View file

@ -1,6 +1,7 @@
use std::path::Path;
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::PythonVersion;
use ruff_python_trivia::CommentRanges;
use crate::package::PackageRoot;
@ -17,6 +18,7 @@ pub(crate) fn check_file_path(
locator: &Locator,
comment_ranges: &CommentRanges,
settings: &LinterSettings,
target_version: PythonVersion,
) -> Vec<Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = vec![];
@ -46,7 +48,7 @@ pub(crate) fn check_file_path(
// flake8-builtins
if settings.rules.enabled(Rule::StdlibModuleShadowing) {
if let Some(diagnostic) = stdlib_module_shadowing(path, settings) {
if let Some(diagnostic) = stdlib_module_shadowing(path, settings, target_version) {
diagnostics.push(diagnostic);
}
}

View file

@ -3,7 +3,7 @@
use ruff_diagnostics::Diagnostic;
use ruff_notebook::CellOffsets;
use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_ast::{ModModule, PySourceType};
use ruff_python_ast::{ModModule, PySourceType, PythonVersion};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::Parsed;
@ -27,6 +27,7 @@ pub(crate) fn check_imports(
package: Option<PackageRoot<'_>>,
source_type: PySourceType,
cell_offsets: Option<&CellOffsets>,
target_version: PythonVersion,
) -> Vec<Diagnostic> {
// Extract all import blocks from the AST.
let tracker = {
@ -52,6 +53,7 @@ pub(crate) fn check_imports(
package,
source_type,
parsed.tokens(),
target_version,
) {
diagnostics.push(diagnostic);
}

View file

@ -1,6 +1,5 @@
use std::path::{Path, PathBuf};
use log::debug;
use path_absolutize::Absolutize;
use crate::registry::RuleSet;
@ -8,43 +7,8 @@ use crate::settings::types::CompiledPerFileIgnoreList;
/// Create a set with codes matching the pattern/code pairs.
pub(crate) fn ignores_from_path(path: &Path, ignore_list: &CompiledPerFileIgnoreList) -> RuleSet {
let file_name = path.file_name().expect("Unable to parse filename");
ignore_list
.iter()
.filter_map(|entry| {
if entry.basename_matcher.is_match(file_name) {
if entry.negated { None } else {
debug!(
"Adding per-file ignores for {:?} due to basename match on {:?}: {:?}",
path,
entry.basename_matcher.glob().regex(),
entry.rules
);
Some(&entry.rules)
}
} else if entry.absolute_matcher.is_match(path) {
if entry.negated { None } else {
debug!(
"Adding per-file ignores for {:?} due to absolute match on {:?}: {:?}",
path,
entry.absolute_matcher.glob().regex(),
entry.rules
);
Some(&entry.rules)
}
} else if entry.negated {
debug!(
"Adding per-file ignores for {:?} due to negated pattern matching neither {:?} nor {:?}: {:?}",
path,
entry.basename_matcher.glob().regex(),
entry.absolute_matcher.glob().regex(),
entry.rules
);
Some(&entry.rules)
} else {
None
}
})
.iter_matches(path, "Adding per-file ignores")
.flatten()
.collect()
}

View file

@ -104,6 +104,8 @@ pub fn check_path(
));
}
let target_version = settings.resolve_target_version(path);
// Run the filesystem-based rules.
if settings
.rules
@ -116,6 +118,7 @@ pub fn check_path(
locator,
comment_ranges,
settings,
target_version,
));
}
@ -158,6 +161,7 @@ pub fn check_path(
source_type,
cell_offsets,
notebook_index,
target_version,
));
}
if use_imports {
@ -171,6 +175,7 @@ pub fn check_path(
package,
source_type,
cell_offsets,
target_version,
);
diagnostics.extend(import_diagnostics);

View file

@ -399,7 +399,7 @@ impl ShadowedKind {
if is_python_builtin(
new_name,
checker.settings.target_version.minor,
checker.target_version().minor,
checker.source_type.is_ipynb(),
) {
return ShadowedKind::BuiltIn;

View file

@ -36,7 +36,7 @@ mod tests {
let diagnostics = test_path(
Path::new("fastapi").join(path).as_path(),
&settings::LinterSettings {
target_version: ruff_python_ast::PythonVersion::PY38,
unresolved_target_version: ruff_python_ast::PythonVersion::PY38,
..settings::LinterSettings::for_rule(rule_code)
},
)?;

View file

@ -226,13 +226,13 @@ fn create_diagnostic(
) -> bool {
let mut diagnostic = Diagnostic::new(
FastApiNonAnnotatedDependency {
py_version: checker.settings.target_version,
py_version: checker.target_version(),
},
parameter.range,
);
let try_generate_fix = || {
let module = if checker.settings.target_version >= PythonVersion::PY39 {
let module = if checker.target_version() >= PythonVersion::PY39 {
"typing"
} else {
"typing_extensions"

View file

@ -128,7 +128,7 @@ mod tests {
let diagnostics = test_path(
Path::new("flake8_annotations/auto_return_type.py"),
&LinterSettings {
target_version: PythonVersion::PY38,
unresolved_target_version: PythonVersion::PY38,
..LinterSettings::for_rules(vec![
Rule::MissingReturnTypeUndocumentedPublicFunction,
Rule::MissingReturnTypePrivateFunction,

View file

@ -523,7 +523,7 @@ fn check_dynamically_typed<F>(
if type_hint_resolves_to_any(
parsed_annotation.expression(),
checker,
checker.settings.target_version,
checker.target_version(),
) {
diagnostics.push(Diagnostic::new(
AnyType { name: func() },
@ -532,7 +532,7 @@ fn check_dynamically_typed<F>(
}
}
} else {
if type_hint_resolves_to_any(annotation, checker, checker.settings.target_version) {
if type_hint_resolves_to_any(annotation, checker, checker.target_version()) {
diagnostics.push(Diagnostic::new(
AnyType { name: func() },
annotation.range(),
@ -725,7 +725,7 @@ pub(crate) fn definition(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
checker.target_version(),
)
})
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits))
@ -756,7 +756,7 @@ pub(crate) fn definition(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
checker.target_version(),
)
})
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits))
@ -826,7 +826,7 @@ pub(crate) fn definition(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
checker.target_version(),
)
})
.map(|(return_type, edits)| {
@ -865,7 +865,7 @@ pub(crate) fn definition(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
checker.target_version(),
)
})
.map(|(return_type, edits)| {

View file

@ -44,7 +44,7 @@ mod tests {
let diagnostics = test_path(
Path::new("flake8_async").join(path),
&LinterSettings {
target_version: PythonVersion::PY310,
unresolved_target_version: PythonVersion::PY310,
..LinterSettings::for_rule(Rule::AsyncFunctionWithTimeout)
},
)?;

View file

@ -108,7 +108,7 @@ pub(crate) fn async_function_with_timeout(checker: &Checker, function_def: &ast:
};
// asyncio.timeout feature was first introduced in Python 3.11
if module == AsyncModule::AsyncIo && checker.settings.target_version < PythonVersion::PY311 {
if module == AsyncModule::AsyncIo && checker.target_version() < PythonVersion::PY311 {
return;
}

View file

@ -100,7 +100,7 @@ mod tests {
let diagnostics = test_path(
Path::new("flake8_bugbear").join(path).as_path(),
&LinterSettings {
target_version,
unresolved_target_version: target_version,
..LinterSettings::for_rule(rule_code)
},
)?;

View file

@ -59,7 +59,7 @@ impl Violation for BatchedWithoutExplicitStrict {
/// B911
pub(crate) fn batched_without_explicit_strict(checker: &Checker, call: &ExprCall) {
if checker.settings.target_version < PythonVersion::PY313 {
if checker.target_version() < PythonVersion::PY313 {
return;
}

View file

@ -78,7 +78,7 @@ pub(crate) fn class_as_data_structure(checker: &Checker, class_def: &ast::StmtCl
// skip `self`
.skip(1)
.all(|param| param.annotation().is_some() && !param.is_variadic())
&& (func_def.parameters.kwonlyargs.is_empty() || checker.settings.target_version >= PythonVersion::PY310)
&& (func_def.parameters.kwonlyargs.is_empty() || checker.target_version() >= PythonVersion::PY310)
// `__init__` should not have complicated logic in it
// only assignments
&& func_def

View file

@ -217,7 +217,7 @@ mod tests {
let diagnostics = test_path(
Path::new("flake8_builtins").join(path).as_path(),
&LinterSettings {
target_version: PythonVersion::PY38,
unresolved_target_version: PythonVersion::PY38,
..LinterSettings::for_rule(rule_code)
},
)?;

View file

@ -68,7 +68,7 @@ pub(crate) fn builtin_argument_shadowing(checker: &Checker, parameter: &Paramete
parameter.name(),
checker.source_type,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.settings.target_version,
checker.target_version(),
) {
// Ignore parameters in lambda expressions.
// (That is the domain of A006.)

View file

@ -99,7 +99,7 @@ pub(crate) fn builtin_attribute_shadowing(
name,
checker.source_type,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.settings.target_version,
checker.target_version(),
) {
// Ignore explicit overrides.
if class_def.decorator_list.iter().any(|decorator| {

View file

@ -61,7 +61,7 @@ pub(crate) fn builtin_import_shadowing(checker: &Checker, alias: &Alias) {
name.as_str(),
checker.source_type,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.settings.target_version,
checker.target_version(),
) {
checker.report_diagnostic(Diagnostic::new(
BuiltinImportShadowing {

View file

@ -44,7 +44,7 @@ pub(crate) fn builtin_lambda_argument_shadowing(checker: &Checker, lambda: &Expr
name,
checker.source_type,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.settings.target_version,
checker.target_version(),
) {
checker.report_diagnostic(Diagnostic::new(
BuiltinLambdaArgumentShadowing {

View file

@ -63,7 +63,7 @@ pub(crate) fn builtin_variable_shadowing(checker: &Checker, name: &str, range: T
name,
checker.source_type,
&checker.settings.flake8_builtins.builtins_ignorelist,
checker.settings.target_version,
checker.target_version(),
) {
checker.report_diagnostic(Diagnostic::new(
BuiltinVariableShadowing {

View file

@ -3,7 +3,7 @@ use std::path::{Component, Path, PathBuf};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::PySourceType;
use ruff_python_ast::{PySourceType, PythonVersion};
use ruff_python_stdlib::path::is_module_file;
use ruff_python_stdlib::sys::is_known_standard_library;
use ruff_text_size::TextRange;
@ -69,6 +69,7 @@ impl Violation for StdlibModuleShadowing {
pub(crate) fn stdlib_module_shadowing(
mut path: &Path,
settings: &LinterSettings,
target_version: PythonVersion,
) -> Option<Diagnostic> {
if !PySourceType::try_from_path(path).is_some_and(PySourceType::is_py_file) {
return None;
@ -98,7 +99,7 @@ pub(crate) fn stdlib_module_shadowing(
let module_name = components.next()?;
if is_allowed_module(settings, &module_name) {
if is_allowed_module(settings, target_version, &module_name) {
return None;
}
@ -129,7 +130,7 @@ fn get_prefix<'a>(settings: &'a LinterSettings, path: &Path) -> Option<&'a PathB
prefix
}
fn is_allowed_module(settings: &LinterSettings, module: &str) -> bool {
fn is_allowed_module(settings: &LinterSettings, version: PythonVersion, module: &str) -> bool {
// Shadowing private stdlib modules is okay.
// https://github.com/astral-sh/ruff/issues/12949
if module.starts_with('_') && !module.starts_with("__") {
@ -145,5 +146,5 @@ fn is_allowed_module(settings: &LinterSettings, module: &str) -> bool {
return true;
}
!is_known_standard_library(settings.target_version.minor, module)
!is_known_standard_library(version.minor, module)
}

View file

@ -30,7 +30,7 @@ mod tests {
let diagnostics = test_path(
Path::new("flake8_future_annotations").join(path).as_path(),
&settings::LinterSettings {
target_version: PythonVersion::PY37,
unresolved_target_version: PythonVersion::PY37,
..settings::LinterSettings::for_rule(Rule::FutureRewritableTypeAnnotation)
},
)?;
@ -49,7 +49,7 @@ mod tests {
let diagnostics = test_path(
Path::new("flake8_future_annotations").join(path).as_path(),
&settings::LinterSettings {
target_version: PythonVersion::PY37,
unresolved_target_version: PythonVersion::PY37,
..settings::LinterSettings::for_rule(Rule::FutureRequiredTypeAnnotation)
},
)?;

View file

@ -189,7 +189,7 @@ mod tests {
let diagnostics = test_path(
Path::new("flake8_pyi").join(path).as_path(),
&settings::LinterSettings {
target_version: PythonVersion::PY38,
unresolved_target_version: PythonVersion::PY38,
..settings::LinterSettings::for_rule(rule_code)
},
)?;

View file

@ -560,7 +560,7 @@ fn replace_custom_typevar_with_self(
/// This is because it was added to the `typing` module on Python 3.11,
/// but is available from the backport package `typing_extensions` on all versions.
fn import_self(checker: &Checker, position: TextSize) -> Result<(Edit, String), ResolutionError> {
let source_module = if checker.settings.target_version >= PythonVersion::PY311 {
let source_module = if checker.target_version() >= PythonVersion::PY311 {
"typing"
} else {
"typing_extensions"

View file

@ -67,7 +67,7 @@ pub(crate) fn no_return_argument_annotation(checker: &Checker, parameters: &ast:
if is_no_return(annotation, checker) {
checker.report_diagnostic(Diagnostic::new(
NoReturnArgumentAnnotationInStub {
module: if checker.settings.target_version >= PythonVersion::PY311 {
module: if checker.target_version() >= PythonVersion::PY311 {
TypingModule::Typing
} else {
TypingModule::TypingExtensions

View file

@ -215,7 +215,7 @@ fn replace_with_self_fix(
let semantic = checker.semantic();
let (self_import, self_binding) = {
let source_module = if checker.settings.target_version >= PythonVersion::PY311 {
let source_module = if checker.target_version() >= PythonVersion::PY311 {
"typing"
} else {
"typing_extensions"

View file

@ -56,7 +56,7 @@ impl Violation for Pep484StylePositionalOnlyParameter {
/// PYI063
pub(crate) fn pep_484_positional_parameter(checker: &Checker, function_def: &ast::StmtFunctionDef) {
// PEP 570 was introduced in Python 3.8.
if checker.settings.target_version < PythonVersion::PY38 {
if checker.target_version() < PythonVersion::PY38 {
return;
}

View file

@ -112,9 +112,7 @@ pub(crate) fn redundant_none_literal<'a>(checker: &Checker, literal_expr: &'a Ex
let union_kind = if literal_elements.is_empty() {
UnionKind::NoUnion
} else if (checker.settings.target_version >= PythonVersion::PY310)
|| checker.source_type.is_stub()
{
} else if (checker.target_version() >= PythonVersion::PY310) || checker.source_type.is_stub() {
UnionKind::BitOr
} else {
UnionKind::TypingOptional

View file

@ -667,7 +667,7 @@ pub(crate) fn type_alias_without_annotation(checker: &Checker, value: &Expr, tar
return;
}
let module = if checker.settings.target_version >= PythonVersion::PY310 {
let module = if checker.target_version() >= PythonVersion::PY310 {
TypingModule::Typing
} else {
TypingModule::TypingExtensions

View file

@ -3,7 +3,7 @@ use flake8_quotes::settings::Quote;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::visitor::{walk_f_string, Visitor};
use ruff_python_ast::{self as ast, AnyStringFlags, StringFlags, StringLike};
use ruff_python_ast::{self as ast, AnyStringFlags, PythonVersion, StringFlags, StringLike};
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::checkers::ast::Checker;
@ -61,7 +61,11 @@ pub(crate) fn avoidable_escaped_quote(checker: &Checker, string_like: StringLike
return;
}
let mut rule_checker = AvoidableEscapedQuoteChecker::new(checker.locator(), checker.settings);
let mut rule_checker = AvoidableEscapedQuoteChecker::new(
checker.locator(),
checker.settings,
checker.target_version(),
);
for part in string_like.parts() {
match part {
@ -88,11 +92,15 @@ struct AvoidableEscapedQuoteChecker<'a> {
}
impl<'a> AvoidableEscapedQuoteChecker<'a> {
fn new(locator: &'a Locator<'a>, settings: &'a LinterSettings) -> Self {
fn new(
locator: &'a Locator<'a>,
settings: &'a LinterSettings,
target_version: PythonVersion,
) -> Self {
Self {
locator,
quotes_settings: &settings.flake8_quotes,
supports_pep701: settings.target_version.supports_pep_701(),
supports_pep701: target_version.supports_pep_701(),
diagnostics: vec![],
}
}

View file

@ -92,7 +92,7 @@ mod tests {
let diagnostics = test_path(
Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings {
target_version: PythonVersion::PY39,
unresolved_target_version: PythonVersion::PY39,
..settings::LinterSettings::for_rule(rule_code)
},
)?;

View file

@ -10,7 +10,6 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::registry::Rule;
use crate::rules::flake8_type_checking::helpers::quote_type_expression;
use crate::settings::LinterSettings;
use ruff_python_ast::PythonVersion;
/// ## What it does
@ -284,7 +283,7 @@ pub(crate) fn quoted_type_alias(
// explicit type aliases require some additional checks to avoid false positives
if checker.semantic().in_annotated_type_alias_value()
&& quotes_are_unremovable(checker.semantic(), expr, checker.settings)
&& quotes_are_unremovable(checker.semantic(), expr, checker.target_version())
{
return;
}
@ -305,7 +304,7 @@ pub(crate) fn quoted_type_alias(
fn quotes_are_unremovable(
semantic: &SemanticModel,
expr: &Expr,
settings: &LinterSettings,
target_version: PythonVersion,
) -> bool {
match expr {
Expr::BinOp(ast::ExprBinOp {
@ -313,11 +312,11 @@ fn quotes_are_unremovable(
}) => {
match op {
Operator::BitOr => {
if settings.target_version < PythonVersion::PY310 {
if target_version < PythonVersion::PY310 {
return true;
}
quotes_are_unremovable(semantic, left, settings)
|| quotes_are_unremovable(semantic, right, settings)
quotes_are_unremovable(semantic, left, target_version)
|| quotes_are_unremovable(semantic, right, target_version)
}
// for now we'll treat uses of other operators as unremovable quotes
// since that would make it an invalid type expression anyways. We skip
@ -330,7 +329,7 @@ fn quotes_are_unremovable(
value,
ctx: ExprContext::Load,
..
}) => quotes_are_unremovable(semantic, value, settings),
}) => quotes_are_unremovable(semantic, value, target_version),
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
// for subscripts we don't know whether it's safe to do at runtime
// since the operation may only be available at type checking time.
@ -338,7 +337,7 @@ fn quotes_are_unremovable(
if !semantic.in_type_checking_block() {
return true;
}
if quotes_are_unremovable(semantic, value, settings) {
if quotes_are_unremovable(semantic, value, target_version) {
return true;
}
// for `typing.Annotated`, only analyze the first argument, since the rest may
@ -347,23 +346,23 @@ fn quotes_are_unremovable(
if semantic.match_typing_qualified_name(&qualified_name, "Annotated") {
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() {
return !elts.is_empty()
&& quotes_are_unremovable(semantic, &elts[0], settings);
&& quotes_are_unremovable(semantic, &elts[0], target_version);
}
return false;
}
}
quotes_are_unremovable(semantic, slice, settings)
quotes_are_unremovable(semantic, slice, target_version)
}
Expr::Attribute(ast::ExprAttribute { value, .. }) => {
// for attributes we also don't know whether it's safe
if !semantic.in_type_checking_block() {
return true;
}
quotes_are_unremovable(semantic, value, settings)
quotes_are_unremovable(semantic, value, target_version)
}
Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => {
for elt in elts {
if quotes_are_unremovable(semantic, elt, settings) {
if quotes_are_unremovable(semantic, elt, target_version) {
return true;
}
}

View file

@ -307,7 +307,7 @@ pub(crate) fn typing_only_runtime_import(
checker.package(),
checker.settings.isort.detect_same_package,
&checker.settings.isort.known_modules,
checker.settings.target_version,
checker.target_version(),
checker.settings.isort.no_sections,
&checker.settings.isort.section_order,
&checker.settings.isort.default_section,

View file

@ -152,7 +152,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
),
// PTH115
// Python 3.9+
["os", "readlink"] if checker.settings.target_version >= PythonVersion::PY39 => {
["os", "readlink"] if checker.target_version() >= PythonVersion::PY39 => {
Some(OsReadlink.into())
}
// PTH208,

View file

@ -3,7 +3,7 @@ use itertools::{EitherOrBoth, Itertools};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::whitespace::trailing_lines_end;
use ruff_python_ast::{PySourceType, Stmt};
use ruff_python_ast::{PySourceType, PythonVersion, Stmt};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::Tokens;
@ -88,6 +88,7 @@ pub(crate) fn organize_imports(
package: Option<PackageRoot<'_>>,
source_type: PySourceType,
tokens: &Tokens,
target_version: PythonVersion,
) -> Option<Diagnostic> {
let indentation = locator.slice(extract_indentation_range(&block.imports, locator));
let indentation = leading_indentation(indentation);
@ -127,7 +128,7 @@ pub(crate) fn organize_imports(
&settings.src,
package,
source_type,
settings.target_version,
target_version,
&settings.isort,
tokens,
);

View file

@ -43,7 +43,7 @@ mod tests {
Path::new("perflint").join(path).as_path(),
&LinterSettings {
preview: PreviewMode::Enabled,
target_version: PythonVersion::PY310,
unresolved_target_version: PythonVersion::PY310,
..LinterSettings::for_rule(rule_code)
},
)?;

View file

@ -89,7 +89,7 @@ impl Violation for TryExceptInLoop {
/// PERF203
pub(crate) fn try_except_in_loop(checker: &Checker, body: &[Stmt]) {
if checker.settings.target_version >= PythonVersion::PY311 {
if checker.target_version() >= PythonVersion::PY311 {
return;
}

View file

@ -218,7 +218,7 @@ mod tests {
let diagnostics = test_snippet(
"PythonFinalizationError",
&LinterSettings {
target_version: ruff_python_ast::PythonVersion::PY312,
unresolved_target_version: ruff_python_ast::PythonVersion::PY312,
..LinterSettings::for_rule(Rule::UndefinedName)
},
);

View file

@ -229,7 +229,7 @@ fn is_first_party(import: &AnyImport, checker: &Checker) -> bool {
checker.package(),
checker.settings.isort.detect_same_package,
&checker.settings.isort.known_modules,
checker.settings.target_version,
checker.target_version(),
checker.settings.isort.no_sections,
&checker.settings.isort.section_order,
&checker.settings.isort.default_section,

View file

@ -211,7 +211,7 @@ pub(crate) fn bad_str_strip_call(checker: &Checker, call: &ast::ExprCall) {
return;
}
let removal = if checker.settings.target_version >= PythonVersion::PY39 {
let removal = if checker.target_version() >= PythonVersion::PY39 {
RemovalKind::for_strip(strip)
} else {
None

View file

@ -76,7 +76,7 @@ pub(crate) fn unnecessary_dunder_call(checker: &Checker, call: &ast::ExprCall) {
}
// If this is an allowed dunder method, abort.
if allowed_dunder_constants(attr, checker.settings.target_version) {
if allowed_dunder_constants(attr, checker.target_version()) {
return;
}

View file

@ -55,7 +55,7 @@ pub(crate) fn useless_exception_statement(checker: &Checker, expr: &ast::StmtExp
return;
};
if is_builtin_exception(func, checker.semantic(), checker.settings.target_version) {
if is_builtin_exception(func, checker.semantic(), checker.target_version()) {
let mut diagnostic = Diagnostic::new(UselessExceptionStatement, expr.range());
diagnostic.set_fix(Fix::unsafe_edit(Edit::insertion(
"raise ".to_string(),

View file

@ -156,7 +156,7 @@ mod tests {
let diagnostics = test_path(
Path::new("pyupgrade/UP041.py"),
&settings::LinterSettings {
target_version: PythonVersion::PY310,
unresolved_target_version: PythonVersion::PY310,
..settings::LinterSettings::for_rule(Rule::TimeoutErrorAlias)
},
)?;
@ -169,7 +169,7 @@ mod tests {
let diagnostics = test_path(
Path::new("pyupgrade/UP040.py"),
&settings::LinterSettings {
target_version: PythonVersion::PY311,
unresolved_target_version: PythonVersion::PY311,
..settings::LinterSettings::for_rule(Rule::NonPEP695TypeAlias)
},
)?;
@ -185,7 +185,7 @@ mod tests {
pyupgrade: pyupgrade::settings::Settings {
keep_runtime_typing: true,
},
target_version: PythonVersion::PY37,
unresolved_target_version: PythonVersion::PY37,
..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation)
},
)?;
@ -201,7 +201,7 @@ mod tests {
pyupgrade: pyupgrade::settings::Settings {
keep_runtime_typing: true,
},
target_version: PythonVersion::PY310,
unresolved_target_version: PythonVersion::PY310,
..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation)
},
)?;
@ -214,7 +214,7 @@ mod tests {
let diagnostics = test_path(
Path::new("pyupgrade/future_annotations.py"),
&settings::LinterSettings {
target_version: PythonVersion::PY37,
unresolved_target_version: PythonVersion::PY37,
..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation)
},
)?;
@ -227,7 +227,7 @@ mod tests {
let diagnostics = test_path(
Path::new("pyupgrade/future_annotations.py"),
&settings::LinterSettings {
target_version: PythonVersion::PY310,
unresolved_target_version: PythonVersion::PY310,
..settings::LinterSettings::for_rule(Rule::NonPEP585Annotation)
},
)?;
@ -240,7 +240,7 @@ mod tests {
let diagnostics = test_path(
Path::new("pyupgrade/future_annotations.py"),
&settings::LinterSettings {
target_version: PythonVersion::PY37,
unresolved_target_version: PythonVersion::PY37,
..settings::LinterSettings::for_rules([
Rule::NonPEP604AnnotationUnion,
Rule::NonPEP604AnnotationOptional,
@ -256,7 +256,7 @@ mod tests {
let diagnostics = test_path(
Path::new("pyupgrade/future_annotations.py"),
&settings::LinterSettings {
target_version: PythonVersion::PY310,
unresolved_target_version: PythonVersion::PY310,
..settings::LinterSettings::for_rules([
Rule::NonPEP604AnnotationUnion,
Rule::NonPEP604AnnotationOptional,
@ -272,7 +272,7 @@ mod tests {
let diagnostics = test_path(
Path::new("pyupgrade/UP017.py"),
&settings::LinterSettings {
target_version: PythonVersion::PY311,
unresolved_target_version: PythonVersion::PY311,
..settings::LinterSettings::for_rule(Rule::DatetimeTimezoneUTC)
},
)?;
@ -286,7 +286,7 @@ mod tests {
Path::new("pyupgrade/UP044.py"),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
target_version: PythonVersion::PY311,
unresolved_target_version: PythonVersion::PY311,
..settings::LinterSettings::for_rule(Rule::NonPEP646Unpack)
},
)?;

View file

@ -722,7 +722,7 @@ pub(crate) fn deprecated_import(checker: &Checker, import_from_stmt: &StmtImport
checker.locator(),
checker.stylist(),
checker.tokens(),
checker.settings.target_version,
checker.target_version(),
);
for (operation, fix) in fixer.without_renames() {

View file

@ -115,7 +115,7 @@ pub(crate) fn outdated_version_block(checker: &Checker, stmt_if: &StmtIf) {
let Some(version) = extract_version(elts) else {
return;
};
let target = checker.settings.target_version;
let target = checker.target_version();
match version_always_less_than(
&version,
target,

View file

@ -106,7 +106,7 @@ impl Violation for NonPEP695GenericClass {
/// UP046
pub(crate) fn non_pep695_generic_class(checker: &Checker, class_def: &StmtClassDef) {
// PEP-695 syntax is only available on Python 3.12+
if checker.settings.target_version < PythonVersion::PY312 {
if checker.target_version() < PythonVersion::PY312 {
return;
}

View file

@ -98,7 +98,7 @@ impl Violation for NonPEP695GenericFunction {
/// UP047
pub(crate) fn non_pep695_generic_function(checker: &Checker, function_def: &StmtFunctionDef) {
// PEP-695 syntax is only available on Python 3.12+
if checker.settings.target_version < PythonVersion::PY312 {
if checker.target_version() < PythonVersion::PY312 {
return;
}

View file

@ -111,7 +111,7 @@ impl Violation for NonPEP695TypeAlias {
/// UP040
pub(crate) fn non_pep695_type_alias_type(checker: &Checker, stmt: &StmtAssign) {
if checker.settings.target_version < PythonVersion::PY312 {
if checker.target_version() < PythonVersion::PY312 {
return;
}
@ -182,7 +182,7 @@ pub(crate) fn non_pep695_type_alias_type(checker: &Checker, stmt: &StmtAssign) {
/// UP040
pub(crate) fn non_pep695_type_alias(checker: &Checker, stmt: &StmtAnnAssign) {
if checker.settings.target_version < PythonVersion::PY312 {
if checker.target_version() < PythonVersion::PY312 {
return;
}

View file

@ -162,7 +162,7 @@ pub(crate) fn timeout_error_alias_handlers(checker: &Checker, handlers: &[Except
};
match expr.as_ref() {
Expr::Name(_) | Expr::Attribute(_) => {
if is_alias(expr, checker.semantic(), checker.settings.target_version) {
if is_alias(expr, checker.semantic(), checker.target_version()) {
atom_diagnostic(checker, expr);
}
}
@ -170,7 +170,7 @@ pub(crate) fn timeout_error_alias_handlers(checker: &Checker, handlers: &[Except
// List of aliases to replace with `TimeoutError`.
let mut aliases: Vec<&Expr> = vec![];
for element in tuple {
if is_alias(element, checker.semantic(), checker.settings.target_version) {
if is_alias(element, checker.semantic(), checker.target_version()) {
aliases.push(element);
}
}
@ -185,7 +185,7 @@ pub(crate) fn timeout_error_alias_handlers(checker: &Checker, handlers: &[Except
/// UP041
pub(crate) fn timeout_error_alias_call(checker: &Checker, func: &Expr) {
if is_alias(func, checker.semantic(), checker.settings.target_version) {
if is_alias(func, checker.semantic(), checker.target_version()) {
atom_diagnostic(checker, func);
}
}
@ -193,7 +193,7 @@ pub(crate) fn timeout_error_alias_call(checker: &Checker, func: &Expr) {
/// UP041
pub(crate) fn timeout_error_alias_raise(checker: &Checker, expr: &Expr) {
if matches!(expr, Expr::Name(_) | Expr::Attribute(_)) {
if is_alias(expr, checker.semantic(), checker.settings.target_version) {
if is_alias(expr, checker.semantic(), checker.target_version()) {
atom_diagnostic(checker, expr);
}
}

View file

@ -98,7 +98,7 @@ pub(crate) fn use_pep585_annotation(checker: &Checker, expr: &Expr, replacement:
checker.semantic(),
)?;
let binding_edit = Edit::range_replacement(binding, expr.range());
let applicability = if checker.settings.target_version >= PythonVersion::PY310 {
let applicability = if checker.target_version() >= PythonVersion::PY310 {
Applicability::Safe
} else {
Applicability::Unsafe
@ -122,7 +122,7 @@ pub(crate) fn use_pep585_annotation(checker: &Checker, expr: &Expr, replacement:
Ok(Fix::applicable_edits(
import_edit,
[reference_edit],
if checker.settings.target_version >= PythonVersion::PY310 {
if checker.target_version() >= PythonVersion::PY310 {
Applicability::Safe
} else {
Applicability::Unsafe

View file

@ -142,7 +142,7 @@ pub(crate) fn non_pep604_annotation(
&& !checker.semantic().in_complex_string_type_definition()
&& is_allowed_value(slice);
let applicability = if checker.settings.target_version >= PythonVersion::PY310 {
let applicability = if checker.target_version() >= PythonVersion::PY310 {
Applicability::Safe
} else {
Applicability::Unsafe

View file

@ -53,7 +53,7 @@ impl Violation for NonPEP646Unpack {
/// UP044
pub(crate) fn use_pep646_unpack(checker: &Checker, expr: &ExprSubscript) {
if checker.settings.target_version < PythonVersion::PY311 {
if checker.target_version() < PythonVersion::PY311 {
return;
}

View file

@ -59,7 +59,7 @@ impl AlwaysFixableViolation for BitCount {
/// FURB161
pub(crate) fn bit_count(checker: &Checker, call: &ExprCall) {
// `int.bit_count()` was added in Python 3.10
if checker.settings.target_version < PythonVersion::PY310 {
if checker.target_version() < PythonVersion::PY310 {
return;
}

View file

@ -82,7 +82,7 @@ impl AlwaysFixableViolation for FromisoformatReplaceZ {
/// FURB162
pub(crate) fn fromisoformat_replace_z(checker: &Checker, call: &ExprCall) {
if checker.settings.target_version < PythonVersion::PY311 {
if checker.target_version() < PythonVersion::PY311 {
return;
}

View file

@ -58,12 +58,7 @@ pub(crate) fn read_whole_file(checker: &Checker, with: &ast::StmtWith) {
}
// First we go through all the items in the statement and find all `open` operations.
let candidates = find_file_opens(
with,
checker.semantic(),
true,
checker.settings.target_version,
);
let candidates = find_file_opens(with, checker.semantic(), true, checker.target_version());
if candidates.is_empty() {
return;
}

View file

@ -69,7 +69,7 @@ impl AlwaysFixableViolation for SliceToRemovePrefixOrSuffix {
/// FURB188
pub(crate) fn slice_to_remove_affix_expr(checker: &Checker, if_expr: &ast::ExprIf) {
if checker.settings.target_version < PythonVersion::PY39 {
if checker.target_version() < PythonVersion::PY39 {
return;
}
@ -100,7 +100,7 @@ pub(crate) fn slice_to_remove_affix_expr(checker: &Checker, if_expr: &ast::ExprI
/// FURB188
pub(crate) fn slice_to_remove_affix_stmt(checker: &Checker, if_stmt: &ast::StmtIf) {
if checker.settings.target_version < PythonVersion::PY39 {
if checker.target_version() < PythonVersion::PY39 {
return;
}
if let Some(removal_data) = affix_removal_data_stmt(if_stmt) {

View file

@ -59,12 +59,7 @@ pub(crate) fn write_whole_file(checker: &Checker, with: &ast::StmtWith) {
}
// First we go through all the items in the statement and find all `open` operations.
let candidates = find_file_opens(
with,
checker.semantic(),
false,
checker.settings.target_version,
);
let candidates = find_file_opens(with, checker.semantic(), false, checker.target_version());
if candidates.is_empty() {
return;
}

View file

@ -129,7 +129,7 @@ mod tests {
extend_markup_names: vec![],
allowed_markup_calls: vec![],
},
target_version: PythonVersion::PY310,
unresolved_target_version: PythonVersion::PY310,
..LinterSettings::for_rule(Rule::IncorrectlyParenthesizedTupleInSubscript)
},
)?;

View file

@ -79,7 +79,7 @@ impl Violation for ClassWithMixedTypeVars {
/// RUF053
pub(crate) fn class_with_mixed_type_vars(checker: &Checker, class_def: &StmtClassDef) {
if checker.settings.target_version < PythonVersion::PY312 {
if checker.target_version() < PythonVersion::PY312 {
return;
}

View file

@ -177,11 +177,11 @@ pub(crate) fn implicit_optional(checker: &Checker, parameters: &Parameters) {
let Some(expr) = type_hint_explicitly_allows_none(
parsed_annotation.expression(),
checker,
checker.settings.target_version,
checker.target_version(),
) else {
continue;
};
let conversion_type = checker.settings.target_version.into();
let conversion_type = checker.target_version().into();
let mut diagnostic =
Diagnostic::new(ImplicitOptional { conversion_type }, expr.range());
@ -192,14 +192,12 @@ pub(crate) fn implicit_optional(checker: &Checker, parameters: &Parameters) {
}
} else {
// Unquoted annotation.
let Some(expr) = type_hint_explicitly_allows_none(
annotation,
checker,
checker.settings.target_version,
) else {
let Some(expr) =
type_hint_explicitly_allows_none(annotation, checker, checker.target_version())
else {
continue;
};
let conversion_type = checker.settings.target_version.into();
let conversion_type = checker.target_version().into();
let mut diagnostic =
Diagnostic::new(ImplicitOptional { conversion_type }, expr.range());

View file

@ -88,7 +88,7 @@ pub(crate) fn subscript_with_parenthesized_tuple(checker: &Checker, subscript: &
// to a syntax error in Python 3.10.
// This is no longer a syntax error starting in Python 3.11
// see https://peps.python.org/pep-0646/#change-1-star-expressions-in-indexes
if checker.settings.target_version <= PythonVersion::PY310
if checker.target_version() <= PythonVersion::PY310
&& !prefer_parentheses
&& tuple_subscript.iter().any(Expr::is_starred_expr)
{

View file

@ -8,6 +8,7 @@ use rustc_hash::FxHashSet;
use std::fmt::{Display, Formatter};
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use types::CompiledPerFileTargetVersionList;
use crate::codes::RuleCodePrefix;
use ruff_macros::CacheKey;
@ -219,7 +220,21 @@ pub struct LinterSettings {
pub per_file_ignores: CompiledPerFileIgnoreList,
pub fix_safety: FixSafetyTable,
pub target_version: PythonVersion,
/// The non-path-resolved Python version specified by the `target-version` input option.
///
/// If you have a `Checker` available, see its `target_version` method instead.
///
/// Otherwise, see [`LinterSettings::resolve_target_version`] for a way to obtain the Python
/// version for a given file, while respecting the overrides in `per_file_target_version`.
pub unresolved_target_version: PythonVersion,
/// Path-specific overrides to `unresolved_target_version`.
///
/// If you have a `Checker` available, see its `target_version` method instead.
///
/// Otherwise, see [`LinterSettings::resolve_target_version`] for a way to check a given
/// [`Path`] against these patterns, while falling back to `unresolved_target_version` if none
/// of them match.
pub per_file_target_version: CompiledPerFileTargetVersionList,
pub preview: PreviewMode,
pub explicit_preview_rules: bool,
@ -281,7 +296,8 @@ impl Display for LinterSettings {
self.per_file_ignores,
self.fix_safety | nested,
self.target_version,
self.unresolved_target_version,
self.per_file_target_version,
self.preview,
self.explicit_preview_rules,
self.extension | debug,
@ -361,7 +377,7 @@ impl LinterSettings {
pub fn for_rule(rule_code: Rule) -> Self {
Self {
rules: RuleTable::from_iter([rule_code]),
target_version: PythonVersion::latest(),
unresolved_target_version: PythonVersion::latest(),
..Self::default()
}
}
@ -369,7 +385,7 @@ impl LinterSettings {
pub fn for_rules(rules: impl IntoIterator<Item = Rule>) -> Self {
Self {
rules: RuleTable::from_iter(rules),
target_version: PythonVersion::latest(),
unresolved_target_version: PythonVersion::latest(),
..Self::default()
}
}
@ -377,7 +393,8 @@ impl LinterSettings {
pub fn new(project_root: &Path) -> Self {
Self {
exclude: FilePatternSet::default(),
target_version: PythonVersion::default(),
unresolved_target_version: PythonVersion::default(),
per_file_target_version: CompiledPerFileTargetVersionList::default(),
project_root: project_root.to_path_buf(),
rules: DEFAULT_SELECTORS
.iter()
@ -439,9 +456,20 @@ impl LinterSettings {
#[must_use]
pub fn with_target_version(mut self, target_version: PythonVersion) -> Self {
self.target_version = target_version;
self.unresolved_target_version = target_version;
self
}
/// Resolve the [`PythonVersion`] to use for linting.
///
/// This method respects the per-file version overrides in
/// [`LinterSettings::per_file_target_version`] and falls back on
/// [`LinterSettings::unresolved_target_version`] if none of the override patterns match.
pub fn resolve_target_version(&self, path: &Path) -> PythonVersion {
self.per_file_target_version
.is_match(path)
.unwrap_or(self.unresolved_target_version)
}
}
impl Default for LinterSettings {

View file

@ -5,8 +5,9 @@ use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::string::ToString;
use anyhow::{bail, Result};
use anyhow::{bail, Context, Result};
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
use log::debug;
use pep440_rs::{VersionSpecifier, VersionSpecifiers};
use rustc_hash::FxHashMap;
use serde::{de, Deserialize, Deserializer, Serialize};
@ -274,22 +275,25 @@ impl CacheKey for FilePatternSet {
}
}
/// A glob pattern and associated data for matching file paths.
#[derive(Debug, Clone)]
pub struct PerFileIgnore {
pub(crate) basename: String,
pub(crate) absolute: PathBuf,
pub(crate) negated: bool,
pub(crate) rules: RuleSet,
pub struct PerFile<T> {
/// The glob pattern used to construct the [`PerFile`].
basename: String,
/// The same pattern as `basename` but normalized to the project root directory.
absolute: PathBuf,
/// Whether the glob pattern should be negated (e.g. `!*.ipynb`)
negated: bool,
/// The per-file data associated with these glob patterns.
data: T,
}
impl PerFileIgnore {
pub fn new(
mut pattern: String,
prefixes: &[RuleSelector],
project_root: Option<&Path>,
) -> Self {
// Rules in preview are included here even if preview mode is disabled; it's safe to ignore disabled rules
let rules: RuleSet = prefixes.iter().flat_map(RuleSelector::all_rules).collect();
impl<T> PerFile<T> {
/// Construct a new [`PerFile`] from the given glob `pattern` and containing `data`.
///
/// If provided, `project_root` is used to construct a second glob pattern normalized to the
/// project root directory. See [`fs::normalize_path_to`] for more details.
fn new(mut pattern: String, project_root: Option<&Path>, data: T) -> Self {
let negated = pattern.starts_with('!');
if negated {
pattern.drain(..1);
@ -304,11 +308,26 @@ impl PerFileIgnore {
basename: pattern,
absolute,
negated,
rules,
data,
}
}
}
/// Per-file ignored linting rules.
///
/// See [`PerFile`] for details of the representation.
#[derive(Debug, Clone)]
pub struct PerFileIgnore(PerFile<RuleSet>);
impl PerFileIgnore {
pub fn new(pattern: String, prefixes: &[RuleSelector], project_root: Option<&Path>) -> Self {
// Rules in preview are included here even if preview mode is disabled; it's safe to ignore
// disabled rules
let rules: RuleSet = prefixes.iter().flat_map(RuleSelector::all_rules).collect();
Self(PerFile::new(pattern, project_root, rules))
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PatternPrefixPair {
pub pattern: String,
@ -558,15 +577,47 @@ impl Display for RequiredVersion {
/// pattern matching.
pub type IdentifierPattern = glob::Pattern;
#[derive(Debug, Clone, CacheKey)]
pub struct CompiledPerFileIgnore {
/// Like [`PerFile`] but with string globs compiled to [`GlobMatcher`]s for more efficient usage.
#[derive(Debug, Clone)]
pub struct CompiledPerFile<T> {
pub absolute_matcher: GlobMatcher,
pub basename_matcher: GlobMatcher,
pub negated: bool,
pub rules: RuleSet,
pub data: T,
}
impl Display for CompiledPerFileIgnore {
impl<T> CompiledPerFile<T> {
fn new(
absolute_matcher: GlobMatcher,
basename_matcher: GlobMatcher,
negated: bool,
data: T,
) -> Self {
Self {
absolute_matcher,
basename_matcher,
negated,
data,
}
}
}
impl<T> CacheKey for CompiledPerFile<T>
where
T: CacheKey,
{
fn cache_key(&self, state: &mut CacheKeyHasher) {
self.absolute_matcher.cache_key(state);
self.basename_matcher.cache_key(state);
self.negated.cache_key(state);
self.data.cache_key(state);
}
}
impl<T> Display for CompiledPerFile<T>
where
T: Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
display_settings! {
formatter = f,
@ -574,52 +625,130 @@ impl Display for CompiledPerFileIgnore {
self.absolute_matcher | globmatcher,
self.basename_matcher | globmatcher,
self.negated,
self.rules,
self.data,
]
}
Ok(())
}
}
#[derive(Debug, Clone, CacheKey, Default)]
pub struct CompiledPerFileIgnoreList {
// Ordered as (absolute path matcher, basename matcher, rules)
ignores: Vec<CompiledPerFileIgnore>,
/// A sequence of [`CompiledPerFile<T>`].
#[derive(Debug, Clone, Default)]
pub struct CompiledPerFileList<T> {
inner: Vec<CompiledPerFile<T>>,
}
impl CompiledPerFileIgnoreList {
/// Given a list of patterns, create a `GlobSet`.
pub fn resolve(per_file_ignores: Vec<PerFileIgnore>) -> Result<Self> {
let ignores: Result<Vec<_>> = per_file_ignores
.into_iter()
.map(|per_file_ignore| {
// Construct absolute path matcher.
let absolute_matcher =
Glob::new(&per_file_ignore.absolute.to_string_lossy())?.compile_matcher();
// Construct basename matcher.
let basename_matcher = Glob::new(&per_file_ignore.basename)?.compile_matcher();
Ok(CompiledPerFileIgnore {
absolute_matcher,
basename_matcher,
negated: per_file_ignore.negated,
rules: per_file_ignore.rules,
})
})
.collect();
Ok(Self { ignores: ignores? })
impl<T> CacheKey for CompiledPerFileList<T>
where
T: CacheKey,
{
fn cache_key(&self, state: &mut CacheKeyHasher) {
self.inner.cache_key(state);
}
}
impl Display for CompiledPerFileIgnoreList {
impl<T> CompiledPerFileList<T> {
/// Given a list of [`PerFile`] patterns, create a compiled set of globs.
///
/// Returns an error if either of the glob patterns cannot be parsed.
fn resolve(per_file_items: impl IntoIterator<Item = PerFile<T>>) -> Result<Self> {
let inner: Result<Vec<_>> = per_file_items
.into_iter()
.map(|per_file_ignore| {
// Construct absolute path matcher.
let absolute_matcher = Glob::new(&per_file_ignore.absolute.to_string_lossy())
.with_context(|| format!("invalid glob {:?}", per_file_ignore.absolute))?
.compile_matcher();
// Construct basename matcher.
let basename_matcher = Glob::new(&per_file_ignore.basename)
.with_context(|| format!("invalid glob {:?}", per_file_ignore.basename))?
.compile_matcher();
Ok(CompiledPerFile::new(
absolute_matcher,
basename_matcher,
per_file_ignore.negated,
per_file_ignore.data,
))
})
.collect();
Ok(Self { inner: inner? })
}
pub(crate) fn is_empty(&self) -> bool {
self.inner.is_empty()
}
}
impl<T: std::fmt::Debug> CompiledPerFileList<T> {
/// Return an iterator over the entries in `self` that match the input `path`.
///
/// `debug_label` is used for [`debug!`] messages explaining why certain patterns were matched.
pub(crate) fn iter_matches<'a, 'p>(
&'a self,
path: &'p Path,
debug_label: &'static str,
) -> impl Iterator<Item = &'p T>
where
'a: 'p,
{
let file_name = path.file_name().expect("Unable to parse filename");
self.inner.iter().filter_map(move |entry| {
if entry.basename_matcher.is_match(file_name) {
if entry.negated {
None
} else {
debug!(
"{} for {:?} due to basename match on {:?}: {:?}",
debug_label,
path,
entry.basename_matcher.glob().regex(),
entry.data
);
Some(&entry.data)
}
} else if entry.absolute_matcher.is_match(path) {
if entry.negated {
None
} else {
debug!(
"{} for {:?} due to absolute match on {:?}: {:?}",
debug_label,
path,
entry.absolute_matcher.glob().regex(),
entry.data
);
Some(&entry.data)
}
} else if entry.negated {
debug!(
"{} for {:?} due to negated pattern matching neither {:?} nor {:?}: {:?}",
debug_label,
path,
entry.basename_matcher.glob().regex(),
entry.absolute_matcher.glob().regex(),
entry.data
);
Some(&entry.data)
} else {
None
}
})
}
}
impl<T> Display for CompiledPerFileList<T>
where
T: Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.ignores.is_empty() {
if self.inner.is_empty() {
write!(f, "{{}}")?;
} else {
writeln!(f, "{{")?;
for ignore in &self.ignores {
writeln!(f, "\t{ignore}")?;
for value in &self.inner {
writeln!(f, "\t{value}")?;
}
write!(f, "}}")?;
}
@ -627,11 +756,70 @@ impl Display for CompiledPerFileIgnoreList {
}
}
#[derive(Debug, Clone, CacheKey, Default)]
pub struct CompiledPerFileIgnoreList(CompiledPerFileList<RuleSet>);
impl CompiledPerFileIgnoreList {
/// Given a list of [`PerFileIgnore`] patterns, create a compiled set of globs.
///
/// Returns an error if either of the glob patterns cannot be parsed.
pub fn resolve(per_file_ignores: Vec<PerFileIgnore>) -> Result<Self> {
Ok(Self(CompiledPerFileList::resolve(
per_file_ignores.into_iter().map(|ignore| ignore.0),
)?))
}
}
impl Deref for CompiledPerFileIgnoreList {
type Target = Vec<CompiledPerFileIgnore>;
type Target = CompiledPerFileList<RuleSet>;
fn deref(&self) -> &Self::Target {
&self.ignores
&self.0
}
}
impl Display for CompiledPerFileIgnoreList {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
/// Contains the target Python version for a given glob pattern.
///
/// See [`PerFile`] for details of the representation.
#[derive(Debug, Clone)]
pub struct PerFileTargetVersion(PerFile<ast::PythonVersion>);
impl PerFileTargetVersion {
pub fn new(pattern: String, version: ast::PythonVersion, project_root: Option<&Path>) -> Self {
Self(PerFile::new(pattern, project_root, version))
}
}
#[derive(CacheKey, Clone, Debug, Default)]
pub struct CompiledPerFileTargetVersionList(CompiledPerFileList<ast::PythonVersion>);
impl CompiledPerFileTargetVersionList {
/// Given a list of [`PerFileTargetVersion`] patterns, create a compiled set of globs.
///
/// Returns an error if either of the glob patterns cannot be parsed.
pub fn resolve(per_file_versions: Vec<PerFileTargetVersion>) -> Result<Self> {
Ok(Self(CompiledPerFileList::resolve(
per_file_versions.into_iter().map(|version| version.0),
)?))
}
pub fn is_match(&self, path: &Path) -> Option<ast::PythonVersion> {
self.0
.iter_matches(path, "Setting Python version")
.next()
.copied()
}
}
impl Display for CompiledPerFileTargetVersionList {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}

View file

@ -1,3 +1,5 @@
use std::path::Path;
use ruff_formatter::PrintedRange;
use ruff_python_ast::PySourceType;
use ruff_python_formatter::{format_module_source, FormatModuleError};
@ -10,8 +12,10 @@ pub(crate) fn format(
document: &TextDocument,
source_type: PySourceType,
formatter_settings: &FormatterSettings,
path: Option<&Path>,
) -> crate::Result<Option<String>> {
let format_options = formatter_settings.to_format_options(source_type, document.contents());
let format_options =
formatter_settings.to_format_options(source_type, document.contents(), path);
match format_module_source(document.contents(), format_options) {
Ok(formatted) => {
let formatted = formatted.into_code();
@ -36,8 +40,10 @@ pub(crate) fn format_range(
source_type: PySourceType,
formatter_settings: &FormatterSettings,
range: TextRange,
path: Option<&Path>,
) -> crate::Result<Option<PrintedRange>> {
let format_options = formatter_settings.to_format_options(source_type, document.contents());
let format_options =
formatter_settings.to_format_options(source_type, document.contents(), path);
match ruff_python_formatter::format_range(document.contents(), range, format_options) {
Ok(formatted) => {
@ -56,3 +62,146 @@ pub(crate) fn format_range(
Err(err) => Err(err.into()),
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use insta::assert_snapshot;
use ruff_linter::settings::types::{CompiledPerFileTargetVersionList, PerFileTargetVersion};
use ruff_python_ast::{PySourceType, PythonVersion};
use ruff_text_size::{TextRange, TextSize};
use ruff_workspace::FormatterSettings;
use crate::format::{format, format_range};
use crate::TextDocument;
#[test]
fn format_per_file_version() {
let document = TextDocument::new(r#"
with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a_really_long_baz") as baz:
pass
"#.to_string(), 0);
let per_file_target_version =
CompiledPerFileTargetVersionList::resolve(vec![PerFileTargetVersion::new(
"test.py".to_string(),
PythonVersion::PY310,
Some(Path::new(".")),
)])
.unwrap();
let result = format(
&document,
PySourceType::Python,
&FormatterSettings {
unresolved_target_version: PythonVersion::PY38,
per_file_target_version,
..Default::default()
},
Some(Path::new("test.py")),
)
.expect("Expected no errors when formatting")
.expect("Expected formatting changes");
assert_snapshot!(result, @r#"
with (
open("a_really_long_foo") as foo,
open("a_really_long_bar") as bar,
open("a_really_long_baz") as baz,
):
pass
"#);
// same as above but without the per_file_target_version override
let result = format(
&document,
PySourceType::Python,
&FormatterSettings {
unresolved_target_version: PythonVersion::PY38,
..Default::default()
},
Some(Path::new("test.py")),
)
.expect("Expected no errors when formatting")
.expect("Expected formatting changes");
assert_snapshot!(result, @r#"
with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open(
"a_really_long_baz"
) as baz:
pass
"#);
}
#[test]
fn format_per_file_version_range() -> anyhow::Result<()> {
// prepare a document with formatting changes before and after the intended range (the
// context manager)
let document = TextDocument::new(r#"
def fn(x: str) -> Foo | Bar: return foobar(x)
with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a_really_long_baz") as baz:
pass
sys.exit(
1
)
"#.to_string(), 0);
let start = document.contents().find("with").unwrap();
let end = document.contents().find("pass").unwrap() + "pass".len();
let range = TextRange::new(TextSize::try_from(start)?, TextSize::try_from(end)?);
let per_file_target_version =
CompiledPerFileTargetVersionList::resolve(vec![PerFileTargetVersion::new(
"test.py".to_string(),
PythonVersion::PY310,
Some(Path::new(".")),
)])
.unwrap();
let result = format_range(
&document,
PySourceType::Python,
&FormatterSettings {
unresolved_target_version: PythonVersion::PY38,
per_file_target_version,
..Default::default()
},
range,
Some(Path::new("test.py")),
)
.expect("Expected no errors when formatting")
.expect("Expected formatting changes");
assert_snapshot!(result.as_code(), @r#"
with (
open("a_really_long_foo") as foo,
open("a_really_long_bar") as bar,
open("a_really_long_baz") as baz,
):
pass
"#);
// same as above but without the per_file_target_version override
let result = format_range(
&document,
PySourceType::Python,
&FormatterSettings {
unresolved_target_version: PythonVersion::PY38,
..Default::default()
},
range,
Some(Path::new("test.py")),
)
.expect("Expected no errors when formatting")
.expect("Expected formatting changes");
assert_snapshot!(result.as_code(), @r#"
with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open(
"a_really_long_baz"
) as baz:
pass
"#);
Ok(())
}
}

View file

@ -85,9 +85,10 @@ fn format_text_document(
let settings = query.settings();
// If the document is excluded, return early.
if let Some(file_path) = query.file_path() {
let file_path = query.file_path();
if let Some(file_path) = &file_path {
if is_document_excluded_for_formatting(
&file_path,
file_path,
&settings.file_resolver,
&settings.formatter,
text_document.language_id(),
@ -97,8 +98,13 @@ fn format_text_document(
}
let source = text_document.contents();
let formatted = crate::format::format(text_document, query.source_type(), &settings.formatter)
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
let formatted = crate::format::format(
text_document,
query.source_type(),
&settings.formatter,
file_path.as_deref(),
)
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
let Some(mut formatted) = formatted else {
return Ok(None);
};

View file

@ -49,9 +49,10 @@ fn format_text_document_range(
let settings = query.settings();
// If the document is excluded, return early.
if let Some(file_path) = query.file_path() {
let file_path = query.file_path();
if let Some(file_path) = &file_path {
if is_document_excluded_for_formatting(
&file_path,
file_path,
&settings.file_resolver,
&settings.formatter,
text_document.language_id(),
@ -68,6 +69,7 @@ fn format_text_document_range(
query.source_type(),
&settings.formatter,
range,
file_path.as_deref(),
)
.with_failure_code(lsp_server::ErrorCode::InternalError)?;

View file

@ -303,7 +303,7 @@ impl<'a> ParsedModule<'a> {
// TODO(konstin): Add an options for py/pyi to the UI (2/2)
let options = settings
.formatter
.to_format_options(PySourceType::default(), self.source_code)
.to_format_options(PySourceType::default(), self.source_code, None)
.with_source_map_generation(SourceMapGeneration::Enabled);
format_module_ast(

View file

@ -9,7 +9,7 @@ use std::num::{NonZeroU16, NonZeroU8};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::{anyhow, Result};
use anyhow::{anyhow, Context, Result};
use glob::{glob, GlobError, Paths, PatternError};
use itertools::Itertools;
use regex::Regex;
@ -29,8 +29,9 @@ use ruff_linter::rules::{flake8_import_conventions, isort, pycodestyle};
use ruff_linter::settings::fix_safety_table::FixSafetyTable;
use ruff_linter::settings::rule_table::RuleTable;
use ruff_linter::settings::types::{
CompiledPerFileIgnoreList, ExtensionMapping, FilePattern, FilePatternSet, OutputFormat,
PerFileIgnore, PreviewMode, RequiredVersion, UnsafeFixes,
CompiledPerFileIgnoreList, CompiledPerFileTargetVersionList, ExtensionMapping, FilePattern,
FilePatternSet, OutputFormat, PerFileIgnore, PerFileTargetVersion, PreviewMode,
RequiredVersion, UnsafeFixes,
};
use ruff_linter::settings::{LinterSettings, DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, TASK_TAGS};
use ruff_linter::{
@ -138,6 +139,7 @@ pub struct Configuration {
pub namespace_packages: Option<Vec<PathBuf>>,
pub src: Option<Vec<PathBuf>>,
pub target_version: Option<ast::PythonVersion>,
pub per_file_target_version: Option<Vec<PerFileTargetVersion>>,
// Global formatting options
pub line_length: Option<LineLength>,
@ -174,11 +176,17 @@ impl Configuration {
PreviewMode::Enabled => ruff_python_formatter::PreviewMode::Enabled,
};
let per_file_target_version = CompiledPerFileTargetVersionList::resolve(
self.per_file_target_version.unwrap_or_default(),
)
.context("failed to resolve `per-file-target-version` table")?;
let formatter = FormatterSettings {
exclude: FilePatternSet::try_from_iter(format.exclude.unwrap_or_default())?,
extension: self.extension.clone().unwrap_or_default(),
preview: format_preview,
target_version,
unresolved_target_version: target_version,
per_file_target_version: per_file_target_version.clone(),
line_width: self
.line_length
.map_or(format_defaults.line_width, |length| {
@ -278,7 +286,8 @@ impl Configuration {
exclude: FilePatternSet::try_from_iter(lint.exclude.unwrap_or_default())?,
extension: self.extension.unwrap_or_default(),
preview: lint_preview,
target_version,
unresolved_target_version: target_version,
per_file_target_version,
project_root: project_root.to_path_buf(),
allowed_confusables: lint
.allowed_confusables
@ -533,6 +542,18 @@ impl Configuration {
.map(|src| resolve_src(&src, project_root))
.transpose()?,
target_version: options.target_version.map(ast::PythonVersion::from),
per_file_target_version: options.per_file_target_version.map(|versions| {
versions
.into_iter()
.map(|(pattern, version)| {
PerFileTargetVersion::new(
pattern,
ast::PythonVersion::from(version),
Some(project_root),
)
})
.collect()
}),
// `--extension` is a hidden command-line argument that isn't supported in configuration
// files at present.
extension: None,
@ -580,6 +601,9 @@ impl Configuration {
show_fixes: self.show_fixes.or(config.show_fixes),
src: self.src.or(config.src),
target_version: self.target_version.or(config.target_version),
per_file_target_version: self
.per_file_target_version
.or(config.per_file_target_version),
preview: self.preview.or(config.preview),
extension: self.extension.or(config.extension),

View file

@ -333,6 +333,29 @@ pub struct Options {
)]
pub target_version: Option<PythonVersion>,
/// A list of mappings from glob-style file pattern to Python version to use when checking the
/// corresponding file(s).
///
/// This may be useful for overriding the global Python version settings in `target-version` or
/// `requires-python` for a subset of files. For example, if you have a project with a minimum
/// supported Python version of 3.9 but a subdirectory of developer scripts that want to use a
/// newer feature like the `match` statement from Python 3.10, you can use
/// `per-file-target-version` to specify `"developer_scripts/*.py" = "py310"`.
///
/// This setting is used by the linter to enforce any enabled version-specific lint rules, as
/// well as by the formatter for any version-specific formatting options, such as parenthesizing
/// context managers on Python 3.10+.
#[option(
default = "{}",
value_type = "dict[str, PythonVersion]",
scope = "per-file-target-version",
example = r#"
# Override the project-wide Python version for a developer scripts directory:
"scripts/**.py" = "py312"
"#
)]
pub per_file_target_version: Option<FxHashMap<String, PythonVersion>>,
/// The directories to consider when resolving first- vs. third-party
/// imports.
///

View file

@ -4,11 +4,12 @@ use ruff_formatter::{FormatOptions, IndentStyle, IndentWidth, LineWidth};
use ruff_graph::AnalyzeSettings;
use ruff_linter::display_settings;
use ruff_linter::settings::types::{
ExtensionMapping, FilePattern, FilePatternSet, OutputFormat, UnsafeFixes,
CompiledPerFileTargetVersionList, ExtensionMapping, FilePattern, FilePatternSet, OutputFormat,
UnsafeFixes,
};
use ruff_linter::settings::LinterSettings;
use ruff_macros::CacheKey;
use ruff_python_ast::PySourceType;
use ruff_python_ast::{PySourceType, PythonVersion};
use ruff_python_formatter::{
DocstringCode, DocstringCodeLineWidth, MagicTrailingComma, PreviewMode, PyFormatOptions,
QuoteStyle,
@ -164,7 +165,17 @@ pub struct FormatterSettings {
pub exclude: FilePatternSet,
pub extension: ExtensionMapping,
pub preview: PreviewMode,
pub target_version: ruff_python_ast::PythonVersion,
/// The non-path-resolved Python version specified by the `target-version` input option.
///
/// See [`FormatterSettings::resolve_target_version`] for a way to obtain the Python version for
/// a given file, while respecting the overrides in `per_file_target_version`.
pub unresolved_target_version: PythonVersion,
/// Path-specific overrides to `unresolved_target_version`.
///
/// See [`FormatterSettings::resolve_target_version`] for a way to check a given [`Path`]
/// against these patterns, while falling back to `unresolved_target_version` if none of them
/// match.
pub per_file_target_version: CompiledPerFileTargetVersionList,
pub line_width: LineWidth,
@ -182,7 +193,16 @@ pub struct FormatterSettings {
}
impl FormatterSettings {
pub fn to_format_options(&self, source_type: PySourceType, source: &str) -> PyFormatOptions {
pub fn to_format_options(
&self,
source_type: PySourceType,
source: &str,
path: Option<&Path>,
) -> PyFormatOptions {
let target_version = path
.map(|path| self.resolve_target_version(path))
.unwrap_or(self.unresolved_target_version);
let line_ending = match self.line_ending {
LineEnding::Lf => ruff_formatter::printer::LineEnding::LineFeed,
LineEnding::CrLf => ruff_formatter::printer::LineEnding::CarriageReturnLineFeed,
@ -205,7 +225,7 @@ impl FormatterSettings {
};
PyFormatOptions::from_source_type(source_type)
.with_target_version(self.target_version)
.with_target_version(target_version)
.with_indent_style(self.indent_style)
.with_indent_width(self.indent_width)
.with_quote_style(self.quote_style)
@ -216,6 +236,17 @@ impl FormatterSettings {
.with_docstring_code(self.docstring_code_format)
.with_docstring_code_line_width(self.docstring_code_line_width)
}
/// Resolve the [`PythonVersion`] to use for formatting.
///
/// This method respects the per-file version overrides in
/// [`FormatterSettings::per_file_target_version`] and falls back on
/// [`FormatterSettings::unresolved_target_version`] if none of the override patterns match.
pub fn resolve_target_version(&self, path: &Path) -> PythonVersion {
self.per_file_target_version
.is_match(path)
.unwrap_or(self.unresolved_target_version)
}
}
impl Default for FormatterSettings {
@ -225,7 +256,8 @@ impl Default for FormatterSettings {
Self {
exclude: FilePatternSet::default(),
extension: ExtensionMapping::default(),
target_version: default_options.target_version(),
unresolved_target_version: default_options.target_version(),
per_file_target_version: CompiledPerFileTargetVersionList::default(),
preview: PreviewMode::Disabled,
line_width: default_options.line_width(),
line_ending: LineEnding::Auto,
@ -247,7 +279,8 @@ impl fmt::Display for FormatterSettings {
namespace = "formatter",
fields = [
self.exclude,
self.target_version,
self.unresolved_target_version,
self.per_file_target_version,
self.preview,
self.line_width,
self.line_ending,

10
ruff.schema.json generated
View file

@ -578,6 +578,16 @@
}
}
},
"per-file-target-version": {
"description": "A list of mappings from glob-style file pattern to Python version to use when checking the corresponding file(s).\n\nThis may be useful for overriding the global Python version settings in `target-version` or `requires-python` for a subset of files. For example, if you have a project with a minimum supported Python version of 3.9 but a subdirectory of developer scripts that want to use a newer feature like the `match` statement from Python 3.10, you can use `per-file-target-version` to specify `\"developer_scripts/*.py\" = \"py310\"`.\n\nThis setting is used by the linter to enforce any enabled version-specific lint rules, as well as by the formatter for any version-specific formatting options, such as parenthesizing context managers on Python 3.10+.",
"type": [
"object",
"null"
],
"additionalProperties": {
"$ref": "#/definitions/PythonVersion"
}
},
"preview": {
"description": "Whether to enable preview mode. When preview mode is enabled, Ruff will use unstable rules, fixes, and formatting.",
"type": [