mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 02:12:22 +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
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -3192,6 +3192,7 @@ dependencies = [
|
|||
"thiserror 2.0.12",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-log",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
|
|
|
@ -154,6 +154,7 @@ toml = { version = "0.8.11" }
|
|||
tracing = { version = "0.1.40" }
|
||||
tracing-flame = { version = "0.2.0" }
|
||||
tracing-indicatif = { version = "0.3.6" }
|
||||
tracing-log = { version = "0.2.0" }
|
||||
tracing-subscriber = { version = "0.3.18", default-features = false, features = [
|
||||
"env-filter",
|
||||
"fmt",
|
||||
|
|
|
@ -153,7 +153,11 @@ pub fn run(
|
|||
}));
|
||||
}
|
||||
|
||||
// Don't set up logging for the server command, as it has its own logging setup
|
||||
// and setting the global logger can only be done once.
|
||||
if !matches!(command, Command::Server { .. }) {
|
||||
set_up_logging(global_options.log_level())?;
|
||||
}
|
||||
|
||||
match command {
|
||||
Command::Version { output_format } => {
|
||||
|
|
|
@ -5,12 +5,14 @@ use log::debug;
|
|||
use path_absolutize::path_dedot;
|
||||
|
||||
use ruff_workspace::configuration::Configuration;
|
||||
use ruff_workspace::pyproject;
|
||||
use ruff_workspace::pyproject::{self, find_fallback_target_version};
|
||||
use ruff_workspace::resolver::{
|
||||
resolve_root_settings, ConfigurationTransformer, PyprojectConfig, PyprojectDiscoveryStrategy,
|
||||
Relativity,
|
||||
resolve_root_settings, ConfigurationOrigin, ConfigurationTransformer, PyprojectConfig,
|
||||
PyprojectDiscoveryStrategy,
|
||||
};
|
||||
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
use crate::args::ConfigArguments;
|
||||
|
||||
/// Resolve the relevant settings strategy and defaults for the current
|
||||
|
@ -35,7 +37,11 @@ pub fn resolve(
|
|||
// `pyproject.toml` for _all_ configuration, and resolve paths relative to the
|
||||
// current working directory. (This matches ESLint's behavior.)
|
||||
if let Some(pyproject) = config_arguments.config_file() {
|
||||
let settings = resolve_root_settings(pyproject, Relativity::Cwd, config_arguments)?;
|
||||
let settings = resolve_root_settings(
|
||||
pyproject,
|
||||
config_arguments,
|
||||
ConfigurationOrigin::UserSpecified,
|
||||
)?;
|
||||
debug!(
|
||||
"Using user-specified configuration file at: {}",
|
||||
pyproject.display()
|
||||
|
@ -61,7 +67,8 @@ pub fn resolve(
|
|||
"Using configuration file (via parent) at: {}",
|
||||
pyproject.display()
|
||||
);
|
||||
let settings = resolve_root_settings(&pyproject, Relativity::Parent, config_arguments)?;
|
||||
let settings =
|
||||
resolve_root_settings(&pyproject, config_arguments, ConfigurationOrigin::Ancestor)?;
|
||||
return Ok(PyprojectConfig::new(
|
||||
PyprojectDiscoveryStrategy::Hierarchical,
|
||||
settings,
|
||||
|
@ -74,11 +81,35 @@ pub fn resolve(
|
|||
// end up the "closest" `pyproject.toml` file for every Python file later on, so
|
||||
// these act as the "default" settings.)
|
||||
if let Some(pyproject) = pyproject::find_user_settings_toml() {
|
||||
struct FallbackTransformer<'a> {
|
||||
arguments: &'a ConfigArguments,
|
||||
}
|
||||
|
||||
impl ConfigurationTransformer for FallbackTransformer<'_> {
|
||||
fn transform(&self, mut configuration: Configuration) -> Configuration {
|
||||
// The `requires-python` constraint from the `pyproject.toml` takes precedence
|
||||
// over the `target-version` from the user configuration.
|
||||
let fallback = find_fallback_target_version(&*path_dedot::CWD);
|
||||
if let Some(fallback) = fallback {
|
||||
debug!("Derived `target-version` from found `requires-python`: {fallback:?}");
|
||||
configuration.target_version = Some(fallback.into());
|
||||
}
|
||||
|
||||
self.arguments.transform(configuration)
|
||||
}
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Using configuration file (via cwd) at: {}",
|
||||
pyproject.display()
|
||||
);
|
||||
let settings = resolve_root_settings(&pyproject, Relativity::Cwd, config_arguments)?;
|
||||
let settings = resolve_root_settings(
|
||||
&pyproject,
|
||||
&FallbackTransformer {
|
||||
arguments: config_arguments,
|
||||
},
|
||||
ConfigurationOrigin::UserSettings,
|
||||
)?;
|
||||
return Ok(PyprojectConfig::new(
|
||||
PyprojectDiscoveryStrategy::Hierarchical,
|
||||
settings,
|
||||
|
@ -91,7 +122,24 @@ pub fn resolve(
|
|||
// "closest" `pyproject.toml` file for every Python file later on, so these act
|
||||
// as the "default" settings.)
|
||||
debug!("Using Ruff default settings");
|
||||
let config = config_arguments.transform(Configuration::default());
|
||||
let mut config = config_arguments.transform(Configuration::default());
|
||||
if config.target_version.is_none() {
|
||||
// If we have arrived here we know that there was no `pyproject.toml`
|
||||
// containing a `[tool.ruff]` section found in an ancestral directory.
|
||||
// (This is an implicit requirement in the function
|
||||
// `pyproject::find_settings_toml`.)
|
||||
// However, there may be a `pyproject.toml` with a `requires-python`
|
||||
// specified, and that is what we look for in this step.
|
||||
let fallback = find_fallback_target_version(
|
||||
stdin_filename
|
||||
.as_ref()
|
||||
.unwrap_or(&path_dedot::CWD.as_path()),
|
||||
);
|
||||
if let Some(version) = fallback {
|
||||
debug!("Derived `target-version` from found `requires-python`: {version:?}");
|
||||
}
|
||||
config.target_version = fallback.map(ast::PythonVersion::from);
|
||||
}
|
||||
let settings = config.into_settings(&path_dedot::CWD)?;
|
||||
Ok(PyprojectConfig::new(
|
||||
PyprojectDiscoveryStrategy::Hierarchical,
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -40,6 +40,7 @@ shellexpand = { workspace = true }
|
|||
thiserror = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-log = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["chrono"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -64,6 +64,8 @@ pub(crate) fn init_logging(log_level: LogLevel, log_file: Option<&std::path::Pat
|
|||
|
||||
tracing::subscriber::set_global_default(subscriber)
|
||||
.expect("should be able to set global default subscriber");
|
||||
|
||||
tracing_log::LogTracer::init().unwrap();
|
||||
}
|
||||
|
||||
/// The log level for the server as provided by the client during initialization.
|
||||
|
|
|
@ -9,12 +9,13 @@ use ignore::{WalkBuilder, WalkState};
|
|||
|
||||
use ruff_linter::settings::types::GlobPath;
|
||||
use ruff_linter::{settings::types::FilePattern, settings::types::PreviewMode};
|
||||
use ruff_workspace::pyproject::find_fallback_target_version;
|
||||
use ruff_workspace::resolver::match_exclusion;
|
||||
use ruff_workspace::Settings;
|
||||
use ruff_workspace::{
|
||||
configuration::{Configuration, FormatConfiguration, LintConfiguration, RuleSelection},
|
||||
pyproject::{find_user_settings_toml, settings_toml},
|
||||
resolver::{ConfigurationTransformer, Relativity},
|
||||
resolver::ConfigurationTransformer,
|
||||
};
|
||||
|
||||
use crate::session::settings::{
|
||||
|
@ -64,12 +65,36 @@ impl RuffSettings {
|
|||
/// In the absence of a valid configuration file, it gracefully falls back to
|
||||
/// editor-only settings.
|
||||
pub(crate) fn fallback(editor_settings: &ResolvedEditorSettings, root: &Path) -> RuffSettings {
|
||||
struct FallbackTransformer<'a> {
|
||||
inner: EditorConfigurationTransformer<'a>,
|
||||
}
|
||||
|
||||
impl ConfigurationTransformer for FallbackTransformer<'_> {
|
||||
fn transform(&self, mut configuration: Configuration) -> Configuration {
|
||||
let fallback = find_fallback_target_version(self.inner.1);
|
||||
if let Some(fallback) = fallback {
|
||||
tracing::debug!(
|
||||
"Derived `target-version` from found `requires-python`: {fallback:?}"
|
||||
);
|
||||
configuration.target_version = Some(fallback.into());
|
||||
}
|
||||
|
||||
self.inner.transform(configuration)
|
||||
}
|
||||
}
|
||||
|
||||
find_user_settings_toml()
|
||||
.and_then(|user_settings| {
|
||||
tracing::debug!(
|
||||
"Loading settings from user configuration file: `{}`",
|
||||
user_settings.display()
|
||||
);
|
||||
ruff_workspace::resolver::resolve_root_settings(
|
||||
&user_settings,
|
||||
Relativity::Cwd,
|
||||
&EditorConfigurationTransformer(editor_settings, root),
|
||||
&FallbackTransformer {
|
||||
inner: EditorConfigurationTransformer(editor_settings, root),
|
||||
},
|
||||
ruff_workspace::resolver::ConfigurationOrigin::UserSettings,
|
||||
)
|
||||
.ok()
|
||||
.map(|settings| RuffSettings {
|
||||
|
@ -77,21 +102,45 @@ impl RuffSettings {
|
|||
settings,
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| Self::editor_only(editor_settings, root))
|
||||
.unwrap_or_else(|| {
|
||||
let fallback = find_fallback_target_version(root);
|
||||
if let Some(fallback) = fallback {
|
||||
tracing::debug!(
|
||||
"Derived `target-version` from found `requires-python` for fallback configuration: {fallback:?}"
|
||||
);
|
||||
}
|
||||
|
||||
let configuration = Configuration {
|
||||
target_version: fallback.map(Into::into),
|
||||
..Configuration::default()
|
||||
};
|
||||
Self::with_editor_settings(editor_settings, root, configuration).expect(
|
||||
"editor configuration should merge successfully with default configuration",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Constructs [`RuffSettings`] by merging the editor-defined settings with the
|
||||
/// default configuration.
|
||||
fn editor_only(editor_settings: &ResolvedEditorSettings, root: &Path) -> RuffSettings {
|
||||
let settings = EditorConfigurationTransformer(editor_settings, root)
|
||||
.transform(Configuration::default())
|
||||
.into_settings(root)
|
||||
.expect("editor configuration should merge successfully with default configuration");
|
||||
Self::with_editor_settings(editor_settings, root, Configuration::default())
|
||||
.expect("editor configuration should merge successfully with default configuration")
|
||||
}
|
||||
|
||||
RuffSettings {
|
||||
/// Merges the `configuration` with the editor defined settings.
|
||||
fn with_editor_settings(
|
||||
editor_settings: &ResolvedEditorSettings,
|
||||
root: &Path,
|
||||
configuration: Configuration,
|
||||
) -> anyhow::Result<RuffSettings> {
|
||||
let settings = EditorConfigurationTransformer(editor_settings, root)
|
||||
.transform(configuration)
|
||||
.into_settings(root)?;
|
||||
|
||||
Ok(RuffSettings {
|
||||
path: None,
|
||||
settings,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,10 +189,11 @@ impl RuffSettingsIndex {
|
|||
Ok(Some(pyproject)) => {
|
||||
match ruff_workspace::resolver::resolve_root_settings(
|
||||
&pyproject,
|
||||
Relativity::Parent,
|
||||
&EditorConfigurationTransformer(editor_settings, root),
|
||||
ruff_workspace::resolver::ConfigurationOrigin::Ancestor,
|
||||
) {
|
||||
Ok(settings) => {
|
||||
tracing::debug!("Loaded settings from: `{}`", pyproject.display());
|
||||
respect_gitignore = Some(settings.file_resolver.respect_gitignore);
|
||||
|
||||
index.insert(
|
||||
|
@ -264,10 +314,15 @@ impl RuffSettingsIndex {
|
|||
Ok(Some(pyproject)) => {
|
||||
match ruff_workspace::resolver::resolve_root_settings(
|
||||
&pyproject,
|
||||
Relativity::Parent,
|
||||
&EditorConfigurationTransformer(editor_settings, root),
|
||||
ruff_workspace::resolver::ConfigurationOrigin::Ancestor,
|
||||
) {
|
||||
Ok(settings) => {
|
||||
tracing::debug!(
|
||||
"Loaded settings from: `{}` for `{}`",
|
||||
pyproject.display(),
|
||||
directory.display()
|
||||
);
|
||||
index.write().unwrap().insert(
|
||||
directory,
|
||||
Arc::new(RuffSettings {
|
||||
|
@ -437,8 +492,8 @@ impl ConfigurationTransformer for EditorConfigurationTransformer<'_> {
|
|||
fn open_configuration_file(config_path: &Path) -> crate::Result<Configuration> {
|
||||
ruff_workspace::resolver::resolve_configuration(
|
||||
config_path,
|
||||
Relativity::Cwd,
|
||||
&IdentityTransformer,
|
||||
ruff_workspace::resolver::ConfigurationOrigin::UserSpecified,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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