mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 18:28:24 +00:00
Fallback to requires-python in certain cases when target-version is not found (#16721)
## Summary Restores https://github.com/astral-sh/ruff/pull/16319 after it got dropped from the 0.10 release branch :( --------- Co-authored-by: dylwil3 <dylwil3@gmail.com>
This commit is contained in:
parent
2382fe1f25
commit
595565015b
10 changed files with 2915 additions and 66 deletions
|
@ -42,18 +42,18 @@ impl Pyproject {
|
|||
|
||||
/// Parse a `ruff.toml` file.
|
||||
fn parse_ruff_toml<P: AsRef<Path>>(path: P) -> Result<Options> {
|
||||
let contents = std::fs::read_to_string(path.as_ref())
|
||||
.with_context(|| format!("Failed to read {}", path.as_ref().display()))?;
|
||||
toml::from_str(&contents)
|
||||
.with_context(|| format!("Failed to parse {}", path.as_ref().display()))
|
||||
let path = path.as_ref();
|
||||
let contents = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read {}", path.display()))?;
|
||||
toml::from_str(&contents).with_context(|| format!("Failed to parse {}", path.display()))
|
||||
}
|
||||
|
||||
/// Parse a `pyproject.toml` file.
|
||||
fn parse_pyproject_toml<P: AsRef<Path>>(path: P) -> Result<Pyproject> {
|
||||
let contents = std::fs::read_to_string(path.as_ref())
|
||||
.with_context(|| format!("Failed to read {}", path.as_ref().display()))?;
|
||||
toml::from_str(&contents)
|
||||
.with_context(|| format!("Failed to parse {}", path.as_ref().display()))
|
||||
let path = path.as_ref();
|
||||
let contents = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read {}", path.display()))?;
|
||||
toml::from_str(&contents).with_context(|| format!("Failed to parse {}", path.display()))
|
||||
}
|
||||
|
||||
/// Return `true` if a `pyproject.toml` contains a `[tool.ruff]` section.
|
||||
|
@ -65,20 +65,21 @@ pub fn ruff_enabled<P: AsRef<Path>>(path: P) -> Result<bool> {
|
|||
/// Return the path to the `pyproject.toml` or `ruff.toml` file in a given
|
||||
/// directory.
|
||||
pub fn settings_toml<P: AsRef<Path>>(path: P) -> Result<Option<PathBuf>> {
|
||||
let path = path.as_ref();
|
||||
// Check for `.ruff.toml`.
|
||||
let ruff_toml = path.as_ref().join(".ruff.toml");
|
||||
let ruff_toml = path.join(".ruff.toml");
|
||||
if ruff_toml.is_file() {
|
||||
return Ok(Some(ruff_toml));
|
||||
}
|
||||
|
||||
// Check for `ruff.toml`.
|
||||
let ruff_toml = path.as_ref().join("ruff.toml");
|
||||
let ruff_toml = path.join("ruff.toml");
|
||||
if ruff_toml.is_file() {
|
||||
return Ok(Some(ruff_toml));
|
||||
}
|
||||
|
||||
// Check for `pyproject.toml`.
|
||||
let pyproject_toml = path.as_ref().join("pyproject.toml");
|
||||
let pyproject_toml = path.join("pyproject.toml");
|
||||
if pyproject_toml.is_file() && ruff_enabled(&pyproject_toml)? {
|
||||
return Ok(Some(pyproject_toml));
|
||||
}
|
||||
|
@ -97,6 +98,17 @@ pub fn find_settings_toml<P: AsRef<Path>>(path: P) -> Result<Option<PathBuf>> {
|
|||
Ok(None)
|
||||
}
|
||||
|
||||
/// Derive target version from `required-version` in `pyproject.toml`, if
|
||||
/// such a file exists in an ancestor directory.
|
||||
pub fn find_fallback_target_version<P: AsRef<Path>>(path: P) -> Option<PythonVersion> {
|
||||
for directory in path.as_ref().ancestors() {
|
||||
if let Some(fallback) = get_fallback_target_version(directory) {
|
||||
return Some(fallback);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Find the path to the user-specific `pyproject.toml` or `ruff.toml`, if it
|
||||
/// exists.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
@ -141,9 +153,13 @@ 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) -> Result<Options> {
|
||||
if path.as_ref().ends_with("pyproject.toml") {
|
||||
let pyproject = parse_pyproject_toml(&path)?;
|
||||
pub(super) fn load_options<P: AsRef<Path>>(
|
||||
path: P,
|
||||
version_strategy: &TargetVersionStrategy,
|
||||
) -> Result<Options> {
|
||||
let path = path.as_ref();
|
||||
if path.ends_with("pyproject.toml") {
|
||||
let pyproject = parse_pyproject_toml(path)?;
|
||||
let mut ruff = pyproject
|
||||
.tool
|
||||
.and_then(|tool| tool.ruff)
|
||||
|
@ -157,16 +173,55 @@ pub(super) fn load_options<P: AsRef<Path>>(path: P) -> Result<Options> {
|
|||
}
|
||||
Ok(ruff)
|
||||
} else {
|
||||
let ruff = parse_ruff_toml(path);
|
||||
if let Ok(ruff) = &ruff {
|
||||
let mut ruff = parse_ruff_toml(path);
|
||||
if let Ok(ref mut ruff) = ruff {
|
||||
if ruff.target_version.is_none() {
|
||||
debug!("`project.requires_python` in `pyproject.toml` will not be used to set `target_version` when using `ruff.toml`.");
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ruff
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract `target-version` from `pyproject.toml` in the given directory
|
||||
/// if the file exists and has `requires-python`.
|
||||
fn get_fallback_target_version(dir: &Path) -> Option<PythonVersion> {
|
||||
let pyproject_path = dir.join("pyproject.toml");
|
||||
if !pyproject_path.exists() {
|
||||
return None;
|
||||
}
|
||||
let parsed_pyproject = parse_pyproject_toml(&pyproject_path);
|
||||
|
||||
let pyproject = match parsed_pyproject {
|
||||
Ok(pyproject) => pyproject,
|
||||
Err(err) => {
|
||||
debug!("Failed to find fallback `target-version` due to: {}", err);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(project) = pyproject.project {
|
||||
if let Some(requires_python) = project.requires_python {
|
||||
return get_minimum_supported_version(&requires_python);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
@ -199,6 +254,15 @@ 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;
|
||||
|
|
|
@ -23,7 +23,7 @@ use ruff_linter::package::PackageRoot;
|
|||
use ruff_linter::packaging::is_package;
|
||||
|
||||
use crate::configuration::Configuration;
|
||||
use crate::pyproject::settings_toml;
|
||||
use crate::pyproject::{settings_toml, TargetVersionStrategy};
|
||||
use crate::settings::Settings;
|
||||
use crate::{pyproject, FileResolverSettings};
|
||||
|
||||
|
@ -301,9 +301,10 @@ pub trait ConfigurationTransformer {
|
|||
// resolving the "default" configuration).
|
||||
pub fn resolve_configuration(
|
||||
pyproject: &Path,
|
||||
relativity: Relativity,
|
||||
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));
|
||||
while let Some(path) = next {
|
||||
|
@ -319,7 +320,19 @@ 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() {
|
||||
format!(
|
||||
"Failed to load configuration `{path}`",
|
||||
|
@ -368,10 +381,12 @@ pub fn resolve_configuration(
|
|||
/// `pyproject.toml`.
|
||||
fn resolve_scoped_settings<'a>(
|
||||
pyproject: &'a Path,
|
||||
relativity: Relativity,
|
||||
transformer: &dyn ConfigurationTransformer,
|
||||
origin: ConfigurationOrigin,
|
||||
) -> Result<(&'a Path, Settings)> {
|
||||
let configuration = resolve_configuration(pyproject, relativity, transformer)?;
|
||||
let relativity = Relativity::from(origin);
|
||||
|
||||
let configuration = resolve_configuration(pyproject, transformer, origin)?;
|
||||
let project_root = relativity.resolve(pyproject);
|
||||
let settings = configuration.into_settings(project_root)?;
|
||||
Ok((project_root, settings))
|
||||
|
@ -381,13 +396,37 @@ fn resolve_scoped_settings<'a>(
|
|||
/// configuration with the given [`ConfigurationTransformer`].
|
||||
pub fn resolve_root_settings(
|
||||
pyproject: &Path,
|
||||
relativity: Relativity,
|
||||
transformer: &dyn ConfigurationTransformer,
|
||||
origin: ConfigurationOrigin,
|
||||
) -> Result<Settings> {
|
||||
let (_project_root, settings) = resolve_scoped_settings(pyproject, relativity, transformer)?;
|
||||
let (_project_root, settings) = resolve_scoped_settings(pyproject, transformer, origin)?;
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// How the configuration is provided.
|
||||
pub enum ConfigurationOrigin {
|
||||
/// Origin is unknown to the caller
|
||||
Unknown,
|
||||
/// User specified path to specific configuration file
|
||||
UserSpecified,
|
||||
/// User-level configuration (e.g. in `~/.config/ruff/pyproject.toml`)
|
||||
UserSettings,
|
||||
/// In parent or higher ancestor directory of path
|
||||
Ancestor,
|
||||
}
|
||||
|
||||
impl From<ConfigurationOrigin> for Relativity {
|
||||
fn from(value: ConfigurationOrigin) -> Self {
|
||||
match value {
|
||||
ConfigurationOrigin::Unknown => Self::Parent,
|
||||
ConfigurationOrigin::UserSpecified => Self::Cwd,
|
||||
ConfigurationOrigin::UserSettings => Self::Cwd,
|
||||
ConfigurationOrigin::Ancestor => Self::Parent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find all Python (`.py`, `.pyi` and `.ipynb` files) in a set of paths.
|
||||
pub fn python_files_in_path<'a>(
|
||||
paths: &[PathBuf],
|
||||
|
@ -411,8 +450,11 @@ pub fn python_files_in_path<'a>(
|
|||
for ancestor in path.ancestors() {
|
||||
if seen.insert(ancestor) {
|
||||
if let Some(pyproject) = settings_toml(ancestor)? {
|
||||
let (root, settings) =
|
||||
resolve_scoped_settings(&pyproject, Relativity::Parent, transformer)?;
|
||||
let (root, settings) = resolve_scoped_settings(
|
||||
&pyproject,
|
||||
transformer,
|
||||
ConfigurationOrigin::Ancestor,
|
||||
)?;
|
||||
resolver.add(root, settings);
|
||||
// We found the closest configuration.
|
||||
break;
|
||||
|
@ -564,8 +606,8 @@ impl ParallelVisitor for PythonFilesVisitor<'_, '_> {
|
|||
match settings_toml(entry.path()) {
|
||||
Ok(Some(pyproject)) => match resolve_scoped_settings(
|
||||
&pyproject,
|
||||
Relativity::Parent,
|
||||
self.transformer,
|
||||
ConfigurationOrigin::Ancestor,
|
||||
) {
|
||||
Ok((root, settings)) => {
|
||||
self.global.resolver.write().unwrap().add(root, settings);
|
||||
|
@ -699,7 +741,7 @@ pub fn python_file_at_path(
|
|||
for ancestor in path.ancestors() {
|
||||
if let Some(pyproject) = settings_toml(ancestor)? {
|
||||
let (root, settings) =
|
||||
resolve_scoped_settings(&pyproject, Relativity::Parent, transformer)?;
|
||||
resolve_scoped_settings(&pyproject, transformer, ConfigurationOrigin::Unknown)?;
|
||||
resolver.add(root, settings);
|
||||
break;
|
||||
}
|
||||
|
@ -883,7 +925,7 @@ mod tests {
|
|||
use crate::pyproject::find_settings_toml;
|
||||
use crate::resolver::{
|
||||
is_file_excluded, match_exclusion, python_files_in_path, resolve_root_settings,
|
||||
ConfigurationTransformer, PyprojectConfig, PyprojectDiscoveryStrategy, Relativity,
|
||||
ConfigurationOrigin, ConfigurationTransformer, PyprojectConfig, PyprojectDiscoveryStrategy,
|
||||
ResolvedFile, Resolver,
|
||||
};
|
||||
use crate::settings::Settings;
|
||||
|
@ -904,8 +946,8 @@ mod tests {
|
|||
PyprojectDiscoveryStrategy::Hierarchical,
|
||||
resolve_root_settings(
|
||||
&find_settings_toml(&package_root)?.unwrap(),
|
||||
Relativity::Parent,
|
||||
&NoOpTransformer,
|
||||
ConfigurationOrigin::Ancestor,
|
||||
)?,
|
||||
None,
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue