mirror of https://github.com/astral-sh/ruff
fix: target-version fallback with extend
This commit is contained in:
parent
857fd4f683
commit
e35fa57c85
|
|
@ -76,7 +76,7 @@ linter.rules.should_fix = [
|
||||||
linter.per_file_ignores = {}
|
linter.per_file_ignores = {}
|
||||||
linter.safety_table.forced_safe = []
|
linter.safety_table.forced_safe = []
|
||||||
linter.safety_table.forced_unsafe = []
|
linter.safety_table.forced_unsafe = []
|
||||||
linter.unresolved_target_version = 3.10
|
linter.unresolved_target_version = 3.11
|
||||||
linter.per_file_target_version = {}
|
linter.per_file_target_version = {}
|
||||||
linter.preview = disabled
|
linter.preview = disabled
|
||||||
linter.explicit_preview_rules = false
|
linter.explicit_preview_rules = false
|
||||||
|
|
@ -264,7 +264,7 @@ linter.ruff.parenthesize_tuple_in_subscript = false
|
||||||
|
|
||||||
# Formatter Settings
|
# Formatter Settings
|
||||||
formatter.exclude = []
|
formatter.exclude = []
|
||||||
formatter.unresolved_target_version = 3.10
|
formatter.unresolved_target_version = 3.11
|
||||||
formatter.per_file_target_version = {}
|
formatter.per_file_target_version = {}
|
||||||
formatter.preview = disabled
|
formatter.preview = disabled
|
||||||
formatter.line_width = 88
|
formatter.line_width = 88
|
||||||
|
|
@ -279,7 +279,7 @@ formatter.docstring_code_line_width = dynamic
|
||||||
# Analyze Settings
|
# Analyze Settings
|
||||||
analyze.exclude = []
|
analyze.exclude = []
|
||||||
analyze.preview = disabled
|
analyze.preview = disabled
|
||||||
analyze.target_version = 3.10
|
analyze.target_version = 3.11
|
||||||
analyze.string_imports = disabled
|
analyze.string_imports = disabled
|
||||||
analyze.extension = ExtensionMapping({})
|
analyze.extension = ExtensionMapping({})
|
||||||
analyze.include_dependencies = {}
|
analyze.include_dependencies = {}
|
||||||
|
|
|
||||||
|
|
@ -491,7 +491,7 @@ impl Default for LinterSettings {
|
||||||
/// unset. In contrast, we want to default to `PythonVersion::default()` for lint rules. These
|
/// unset. In contrast, we want to default to `PythonVersion::default()` for lint rules. These
|
||||||
/// correspond to the [`TargetVersion::parser_version`] and [`TargetVersion::linter_version`]
|
/// correspond to the [`TargetVersion::parser_version`] and [`TargetVersion::linter_version`]
|
||||||
/// methods, respectively.
|
/// methods, respectively.
|
||||||
#[derive(Debug, Clone, Copy, CacheKey)]
|
#[derive(Debug, Clone, Copy, CacheKey, PartialEq, Eq)]
|
||||||
pub struct TargetVersion(pub Option<PythonVersion>);
|
pub struct TargetVersion(pub Option<PythonVersion>);
|
||||||
|
|
||||||
impl TargetVersion {
|
impl TargetVersion {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ use std::str::FromStr;
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
use glob::{GlobError, Paths, PatternError, glob};
|
use glob::{GlobError, Paths, PatternError, glob};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use log::debug;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rustc_hash::{FxHashMap, FxHashSet};
|
use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
use shellexpand;
|
use shellexpand;
|
||||||
|
|
@ -54,6 +55,8 @@ use crate::options::{
|
||||||
McCabeOptions, Options, Pep8NamingOptions, PyUpgradeOptions, PycodestyleOptions,
|
McCabeOptions, Options, Pep8NamingOptions, PyUpgradeOptions, PycodestyleOptions,
|
||||||
PydoclintOptions, PydocstyleOptions, PyflakesOptions, PylintOptions, RuffOptions,
|
PydoclintOptions, PydocstyleOptions, PyflakesOptions, PylintOptions, RuffOptions,
|
||||||
};
|
};
|
||||||
|
use crate::pyproject;
|
||||||
|
use crate::resolver::ConfigurationOrigin;
|
||||||
use crate::settings::{
|
use crate::settings::{
|
||||||
EXCLUDE, FileResolverSettings, FormatterSettings, INCLUDE, INCLUDE_PREVIEW, LineEnding,
|
EXCLUDE, FileResolverSettings, FormatterSettings, INCLUDE, INCLUDE_PREVIEW, LineEnding,
|
||||||
Settings,
|
Settings,
|
||||||
|
|
@ -626,6 +629,23 @@ impl Configuration {
|
||||||
analyze: self.analyze.combine(config.analyze),
|
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)]
|
#[derive(Clone, Debug, Default)]
|
||||||
|
|
|
||||||
|
|
@ -135,10 +135,7 @@ pub fn find_user_settings_toml() -> Option<PathBuf> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load `Options` from a `pyproject.toml` or `ruff.toml` file.
|
/// Load `Options` from a `pyproject.toml` or `ruff.toml` file.
|
||||||
pub(super) fn load_options<P: AsRef<Path>>(
|
pub(super) fn load_options<P: AsRef<Path>>(path: P) -> Result<Options> {
|
||||||
path: P,
|
|
||||||
version_strategy: &TargetVersionStrategy,
|
|
||||||
) -> Result<Options> {
|
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
if path.ends_with("pyproject.toml") {
|
if path.ends_with("pyproject.toml") {
|
||||||
let pyproject = parse_pyproject_toml(path)?;
|
let pyproject = parse_pyproject_toml(path)?;
|
||||||
|
|
@ -155,31 +152,15 @@ pub(super) fn load_options<P: AsRef<Path>>(
|
||||||
}
|
}
|
||||||
Ok(ruff)
|
Ok(ruff)
|
||||||
} else {
|
} else {
|
||||||
let mut ruff = parse_ruff_toml(path);
|
let ruff = parse_ruff_toml(path);
|
||||||
if let Ok(ref mut ruff) = ruff {
|
if let Ok(ruff) = ruff {
|
||||||
if ruff.target_version.is_none() {
|
if ruff.target_version.is_none() {
|
||||||
debug!("No `target-version` found in `ruff.toml`");
|
debug!("No `target-version` found in `{}`", path.display());
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ use ruff_linter::package::PackageRoot;
|
||||||
use ruff_linter::packaging::is_package;
|
use ruff_linter::packaging::is_package;
|
||||||
|
|
||||||
use crate::configuration::Configuration;
|
use crate::configuration::Configuration;
|
||||||
use crate::pyproject::{TargetVersionStrategy, settings_toml};
|
use crate::pyproject::settings_toml;
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
use crate::{FileResolverSettings, pyproject};
|
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
|
// file at least twice (possibly more than twice, since we'll also parse it when
|
||||||
// resolving the "default" configuration).
|
// resolving the "default" configuration).
|
||||||
pub fn resolve_configuration(
|
pub fn resolve_configuration(
|
||||||
pyproject: &Path,
|
initial_config_path: &Path,
|
||||||
transformer: &dyn ConfigurationTransformer,
|
transformer: &dyn ConfigurationTransformer,
|
||||||
origin: ConfigurationOrigin,
|
origin: ConfigurationOrigin,
|
||||||
) -> Result<Configuration> {
|
) -> Result<Configuration> {
|
||||||
let relativity = Relativity::from(origin);
|
let relativity = Relativity::from(origin);
|
||||||
let mut configurations = indexmap::IndexMap::new();
|
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 {
|
while let Some(path) = next {
|
||||||
if configurations.contains_key(&path) {
|
if configurations.contains_key(&path) {
|
||||||
bail!(format!(
|
bail!(format!(
|
||||||
|
|
@ -319,20 +319,7 @@ pub fn resolve_configuration(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the current path.
|
let options = pyproject::load_options(&path).with_context(|| {
|
||||||
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(|| {
|
|
||||||
if configurations.is_empty() {
|
if configurations.is_empty() {
|
||||||
format!(
|
format!(
|
||||||
"Failed to load configuration `{path}`",
|
"Failed to load configuration `{path}`",
|
||||||
|
|
@ -374,6 +361,9 @@ pub fn resolve_configuration(
|
||||||
for extend in configurations {
|
for extend in configurations {
|
||||||
configuration = configuration.combine(extend);
|
configuration = configuration.combine(extend);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let configuration = configuration.apply_fallbacks(origin, initial_config_path);
|
||||||
|
|
||||||
Ok(transformer.transform(configuration))
|
Ok(transformer.transform(configuration))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -944,7 +934,10 @@ mod tests {
|
||||||
use path_absolutize::Absolutize;
|
use path_absolutize::Absolutize;
|
||||||
use tempfile::TempDir;
|
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::configuration::Configuration;
|
||||||
use crate::pyproject::find_settings_toml;
|
use crate::pyproject::find_settings_toml;
|
||||||
|
|
@ -1131,4 +1124,47 @@ mod tests {
|
||||||
&make_exclusion(exclude),
|
&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(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue