Use ast::PythonVersion internally in the formatter and linter (#16170)

## Summary

This PR updates the formatter and linter to use the `PythonVersion`
struct from the `ruff_python_ast` crate internally. While this doesn't
remove the need for the `linter::PythonVersion` enum, it does remove the
`formatter::PythonVersion` enum and limits the use in the linter to
deserializing from CLI arguments and config files and moves most of the
remaining methods to the `ast::PythonVersion` struct.

## Test Plan

Existing tests, with some inputs and outputs updated to reflect the new
(de)serialization format. I think these are test-specific and shouldn't
affect any external (de)serialization.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
Brent Westbrook 2025-02-18 12:03:13 -05:00 committed by GitHub
parent 0868e73d2c
commit a9efdea113
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
153 changed files with 456 additions and 539 deletions

View file

@ -30,13 +30,14 @@ 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, PythonVersion, RequiredVersion, UnsafeFixes,
PerFileIgnore, PreviewMode, RequiredVersion, UnsafeFixes,
};
use ruff_linter::settings::{LinterSettings, DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, TASK_TAGS};
use ruff_linter::{
fs, warn_user_once, warn_user_once_by_id, warn_user_once_by_message, RuleSelector,
RUFF_PKG_VERSION,
};
use ruff_python_ast as ast;
use ruff_python_formatter::{
DocstringCode, DocstringCodeLineWidth, MagicTrailingComma, QuoteStyle,
};
@ -136,7 +137,7 @@ pub struct Configuration {
pub builtins: Option<Vec<String>>,
pub namespace_packages: Option<Vec<PathBuf>>,
pub src: Option<Vec<PathBuf>>,
pub target_version: Option<PythonVersion>,
pub target_version: Option<ast::PythonVersion>,
// Global formatting options
pub line_length: Option<LineLength>,
@ -177,15 +178,7 @@ impl Configuration {
exclude: FilePatternSet::try_from_iter(format.exclude.unwrap_or_default())?,
extension: self.extension.clone().unwrap_or_default(),
preview: format_preview,
target_version: match target_version {
PythonVersion::Py37 => ruff_python_formatter::PythonVersion::Py37,
PythonVersion::Py38 => ruff_python_formatter::PythonVersion::Py38,
PythonVersion::Py39 => ruff_python_formatter::PythonVersion::Py39,
PythonVersion::Py310 => ruff_python_formatter::PythonVersion::Py310,
PythonVersion::Py311 => ruff_python_formatter::PythonVersion::Py311,
PythonVersion::Py312 => ruff_python_formatter::PythonVersion::Py312,
PythonVersion::Py313 => ruff_python_formatter::PythonVersion::Py313,
},
target_version,
line_width: self
.line_length
.map_or(format_defaults.line_width, |length| {
@ -539,7 +532,7 @@ impl Configuration {
.src
.map(|src| resolve_src(&src, project_root))
.transpose()?,
target_version: options.target_version,
target_version: options.target_version.map(ast::PythonVersion::from),
// `--extension` is a hidden command-line argument that isn't supported in configuration
// files at present.
extension: None,

View file

@ -3848,9 +3848,7 @@ impl From<LintOptionsWire> for LintOptions {
mod tests {
use crate::options::Flake8SelfOptions;
use ruff_linter::rules::flake8_self;
use ruff_linter::settings::types::PythonVersion as LinterPythonVersion;
use ruff_python_ast::name::Name;
use ruff_python_formatter::PythonVersion as FormatterPythonVersion;
#[test]
fn flake8_self_options() {
@ -3898,28 +3896,4 @@ mod tests {
vec![Name::new_static("_foo"), Name::new_static("_bar")]
);
}
#[test]
fn formatter_and_linter_target_version_have_same_default() {
assert_eq!(
FormatterPythonVersion::default().as_tuple(),
LinterPythonVersion::default().as_tuple()
);
}
#[test]
fn formatter_and_linter_target_version_have_same_latest() {
assert_eq!(
FormatterPythonVersion::latest().as_tuple(),
LinterPythonVersion::latest().as_tuple()
);
}
#[test]
fn formatter_and_linter_target_version_have_same_minimal_supported() {
assert_eq!(
FormatterPythonVersion::minimal_supported().as_tuple(),
LinterPythonVersion::minimal_supported().as_tuple()
);
}
}

View file

@ -4,8 +4,9 @@ use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use log::debug;
use pep440_rs::VersionSpecifiers;
use pep440_rs::{Operator, Version, VersionSpecifiers};
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use ruff_linter::settings::types::PythonVersion;
@ -150,8 +151,7 @@ pub(super) fn load_options<P: AsRef<Path>>(path: P) -> Result<Options> {
if ruff.target_version.is_none() {
if let Some(project) = pyproject.project {
if let Some(requires_python) = project.requires_python {
ruff.target_version =
PythonVersion::get_minimum_supported_version(&requires_python);
ruff.target_version = get_minimum_supported_version(&requires_python);
}
}
}
@ -167,6 +167,38 @@ pub(super) fn load_options<P: AsRef<Path>>(path: P) -> Result<Options> {
}
}
/// Infer the minimum supported [`PythonVersion`] from a `requires-python` specifier.
fn get_minimum_supported_version(requires_version: &VersionSpecifiers) -> Option<PythonVersion> {
/// Truncate a version to its major and minor components.
fn major_minor(version: &Version) -> Option<Version> {
let major = version.release().first()?;
let minor = version.release().get(1)?;
Some(Version::new([major, minor]))
}
// Extract the minimum supported version from the specifiers.
let minimum_version = requires_version
.iter()
.filter(|specifier| {
matches!(
specifier.operator(),
Operator::Equal
| Operator::EqualStar
| Operator::ExactEqual
| Operator::TildeEqual
| Operator::GreaterThan
| Operator::GreaterThanEqual
)
})
.filter_map(|specifier| major_minor(specifier.version()))
.min()?;
debug!("Detected minimum supported `requires-python` version: {minimum_version}");
// Find the Python version that matches the minimum supported version.
PythonVersion::iter().find(|version| Version::from(*version) == minimum_version)
}
#[cfg(test)]
mod tests {
use std::fs;

View file

@ -164,7 +164,7 @@ pub struct FormatterSettings {
pub exclude: FilePatternSet,
pub extension: ExtensionMapping,
pub preview: PreviewMode,
pub target_version: ruff_python_formatter::PythonVersion,
pub target_version: ruff_python_ast::PythonVersion,
pub line_width: LineWidth,
@ -247,7 +247,7 @@ impl fmt::Display for FormatterSettings {
namespace = "formatter",
fields = [
self.exclude,
self.target_version | debug,
self.target_version,
self.preview,
self.line_width,
self.line_ending,