This commit is contained in:
Denys Zhak 2025-12-16 16:37:01 -05:00 committed by GitHub
commit 9b075c2973
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 85 additions and 57 deletions

View File

@ -76,7 +76,7 @@ linter.rules.should_fix = [
linter.per_file_ignores = {}
linter.safety_table.forced_safe = []
linter.safety_table.forced_unsafe = []
linter.unresolved_target_version = 3.10
linter.unresolved_target_version = 3.11
linter.per_file_target_version = {}
linter.preview = disabled
linter.explicit_preview_rules = false
@ -264,7 +264,7 @@ linter.ruff.parenthesize_tuple_in_subscript = false
# Formatter Settings
formatter.exclude = []
formatter.unresolved_target_version = 3.10
formatter.unresolved_target_version = 3.11
formatter.per_file_target_version = {}
formatter.preview = disabled
formatter.line_width = 88
@ -279,7 +279,7 @@ formatter.docstring_code_line_width = dynamic
# Analyze Settings
analyze.exclude = []
analyze.preview = disabled
analyze.target_version = 3.10
analyze.target_version = 3.11
analyze.string_imports = disabled
analyze.extension = ExtensionMapping({})
analyze.include_dependencies = {}

View File

@ -497,7 +497,7 @@ impl Default for LinterSettings {
/// unset. In contrast, we want to default to `PythonVersion::default()` for lint rules. These
/// correspond to the [`TargetVersion::parser_version`] and [`TargetVersion::linter_version`]
/// methods, respectively.
#[derive(Debug, Clone, Copy, CacheKey)]
#[derive(Debug, Clone, Copy, CacheKey, PartialEq, Eq)]
pub struct TargetVersion(pub Option<PythonVersion>);
impl TargetVersion {

View File

@ -12,6 +12,7 @@ use std::str::FromStr;
use anyhow::{Context, Result, anyhow};
use glob::{GlobError, Paths, PatternError, glob};
use itertools::Itertools;
use log::debug;
use regex::Regex;
use rustc_hash::{FxHashMap, FxHashSet};
use shellexpand;
@ -54,6 +55,8 @@ use crate::options::{
McCabeOptions, Options, Pep8NamingOptions, PyUpgradeOptions, PycodestyleOptions,
PydoclintOptions, PydocstyleOptions, PyflakesOptions, PylintOptions, RuffOptions,
};
use crate::pyproject;
use crate::resolver::ConfigurationOrigin;
use crate::settings::{
EXCLUDE, FileResolverSettings, FormatterSettings, INCLUDE, INCLUDE_PREVIEW, LineEnding,
Settings,
@ -626,6 +629,23 @@ impl Configuration {
analyze: self.analyze.combine(config.analyze),
}
}
#[must_use]
pub fn apply_fallbacks(
mut self,
origin: ConfigurationOrigin,
initial_config_path: &Path,
) -> Self {
if matches!(origin, ConfigurationOrigin::Ancestor) {
self.target_version = self.target_version.or_else(|| {
let dir = initial_config_path.parent()?;
let fallback = pyproject::find_fallback_target_version(dir)?;
debug!("Derived `target-version` from `requires-python`: {fallback:?}");
Some(fallback.into())
});
}
self
}
}
#[derive(Clone, Debug, Default)]

View File

@ -135,10 +135,7 @@ pub fn find_user_settings_toml() -> Option<PathBuf> {
}
/// Load `Options` from a `pyproject.toml` or `ruff.toml` file.
pub(super) fn load_options<P: AsRef<Path>>(
path: P,
version_strategy: &TargetVersionStrategy,
) -> Result<Options> {
pub(super) fn load_options<P: AsRef<Path>>(path: P) -> Result<Options> {
let path = path.as_ref();
if path.ends_with("pyproject.toml") {
let pyproject = parse_pyproject_toml(path)?;
@ -155,31 +152,15 @@ pub(super) fn load_options<P: AsRef<Path>>(
}
Ok(ruff)
} else {
let mut ruff = parse_ruff_toml(path);
if let Ok(ref mut ruff) = ruff {
let ruff = parse_ruff_toml(path);
if let Ok(ruff) = ruff {
if ruff.target_version.is_none() {
debug!("No `target-version` found in `ruff.toml`");
match version_strategy {
TargetVersionStrategy::UseDefault => {}
TargetVersionStrategy::RequiresPythonFallback => {
if let Some(dir) = path.parent() {
let fallback = get_fallback_target_version(dir);
if let Some(version) = fallback {
debug!(
"Derived `target-version` from `requires-python` in `pyproject.toml`: {version:?}"
);
} else {
debug!(
"No `pyproject.toml` with `requires-python` in same directory; `target-version` unspecified"
);
}
ruff.target_version = fallback;
}
}
}
debug!("No `target-version` found in `{}`", path.display());
}
Ok(ruff)
} else {
ruff
}
ruff
}
}
@ -240,15 +221,6 @@ fn get_minimum_supported_version(requires_version: &VersionSpecifiers) -> Option
PythonVersion::iter().find(|version| Version::from(*version) == minimum_version)
}
/// Strategy for handling missing `target-version` in configuration.
#[derive(Debug)]
pub(super) enum TargetVersionStrategy {
/// Use default `target-version`
UseDefault,
/// Derive from `requires-python` if available
RequiresPythonFallback,
}
#[cfg(test)]
mod tests {
use std::fs;

View File

@ -23,7 +23,7 @@ use ruff_linter::package::PackageRoot;
use ruff_linter::packaging::is_package;
use crate::configuration::Configuration;
use crate::pyproject::{TargetVersionStrategy, settings_toml};
use crate::pyproject::settings_toml;
use crate::settings::Settings;
use crate::{FileResolverSettings, pyproject};
@ -300,13 +300,13 @@ pub trait ConfigurationTransformer {
// file at least twice (possibly more than twice, since we'll also parse it when
// resolving the "default" configuration).
pub fn resolve_configuration(
pyproject: &Path,
initial_config_path: &Path,
transformer: &dyn ConfigurationTransformer,
origin: ConfigurationOrigin,
) -> Result<Configuration> {
let relativity = Relativity::from(origin);
let mut configurations = indexmap::IndexMap::new();
let mut next = Some(fs::normalize_path(pyproject));
let mut next = Some(fs::normalize_path(initial_config_path));
while let Some(path) = next {
if configurations.contains_key(&path) {
bail!(format!(
@ -319,20 +319,7 @@ pub fn resolve_configuration(
));
}
// Resolve the current path.
let version_strategy =
if configurations.is_empty() && matches!(origin, ConfigurationOrigin::Ancestor) {
// For configurations that are discovered by
// walking back from a file, we will attempt to
// infer the `target-version` if it is missing
TargetVersionStrategy::RequiresPythonFallback
} else {
// In all other cases (e.g. for configurations
// inherited via `extend`, or user-level settings)
// we do not attempt to infer a missing `target-version`
TargetVersionStrategy::UseDefault
};
let options = pyproject::load_options(&path, &version_strategy).with_context(|| {
let options = pyproject::load_options(&path).with_context(|| {
if configurations.is_empty() {
format!(
"Failed to load configuration `{path}`",
@ -374,6 +361,9 @@ pub fn resolve_configuration(
for extend in configurations {
configuration = configuration.combine(extend);
}
let configuration = configuration.apply_fallbacks(origin, initial_config_path);
Ok(transformer.transform(configuration))
}
@ -944,7 +934,10 @@ mod tests {
use path_absolutize::Absolutize;
use tempfile::TempDir;
use ruff_linter::settings::types::{FilePattern, GlobPath};
use ruff_linter::settings::{
TargetVersion,
types::{FilePattern, GlobPath, PythonVersion},
};
use crate::configuration::Configuration;
use crate::pyproject::find_settings_toml;
@ -1131,4 +1124,47 @@ mod tests {
&make_exclusion(exclude),
));
}
#[test]
fn extend_respects_target_version() -> Result<()> {
let tmp_dir = TempDir::new()?;
let root = tmp_dir.path();
let ruff_toml = root.join("ruff.toml");
std::fs::write(&ruff_toml, "target-version = \"py310\"")?;
let dot_ruff_toml = root.join(".ruff.toml");
std::fs::write(&dot_ruff_toml, "extend = \"ruff.toml\"")?;
let pyproject_toml = root.join("pyproject.toml");
std::fs::write(
&pyproject_toml,
r#"[project]
name = "repro-ruff"
version = "0.1.0"
requires-python = ">=3.13"
"#,
)?;
let main_py = root.join("main.py");
std::fs::write(
&main_py,
r#"from typing import TypeAlias
A: TypeAlias = str | int
"#,
)?;
let settings = resolve_root_settings(
&dot_ruff_toml,
&NoOpTransformer,
ConfigurationOrigin::Ancestor,
)?;
assert_eq!(
settings.linter.unresolved_target_version,
TargetVersion(Some(PythonVersion::Py310.into()))
);
Ok(())
}
}