mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-02 14:52:01 +00:00
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:
parent
42a5f5ef6a
commit
e7a6c19e3a
78 changed files with 820 additions and 274 deletions
|
@ -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),
|
||||
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue