mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 22:01:47 +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",
|
"thiserror 2.0.12",
|
||||||
"toml",
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"tracing-log",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -154,6 +154,7 @@ toml = { version = "0.8.11" }
|
||||||
tracing = { version = "0.1.40" }
|
tracing = { version = "0.1.40" }
|
||||||
tracing-flame = { version = "0.2.0" }
|
tracing-flame = { version = "0.2.0" }
|
||||||
tracing-indicatif = { version = "0.3.6" }
|
tracing-indicatif = { version = "0.3.6" }
|
||||||
|
tracing-log = { version = "0.2.0" }
|
||||||
tracing-subscriber = { version = "0.3.18", default-features = false, features = [
|
tracing-subscriber = { version = "0.3.18", default-features = false, features = [
|
||||||
"env-filter",
|
"env-filter",
|
||||||
"fmt",
|
"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())?;
|
set_up_logging(global_options.log_level())?;
|
||||||
|
}
|
||||||
|
|
||||||
match command {
|
match command {
|
||||||
Command::Version { output_format } => {
|
Command::Version { output_format } => {
|
||||||
|
|
|
@ -5,12 +5,14 @@ use log::debug;
|
||||||
use path_absolutize::path_dedot;
|
use path_absolutize::path_dedot;
|
||||||
|
|
||||||
use ruff_workspace::configuration::Configuration;
|
use ruff_workspace::configuration::Configuration;
|
||||||
use ruff_workspace::pyproject;
|
use ruff_workspace::pyproject::{self, find_fallback_target_version};
|
||||||
use ruff_workspace::resolver::{
|
use ruff_workspace::resolver::{
|
||||||
resolve_root_settings, ConfigurationTransformer, PyprojectConfig, PyprojectDiscoveryStrategy,
|
resolve_root_settings, ConfigurationOrigin, ConfigurationTransformer, PyprojectConfig,
|
||||||
Relativity,
|
PyprojectDiscoveryStrategy,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use ruff_python_ast as ast;
|
||||||
|
|
||||||
use crate::args::ConfigArguments;
|
use crate::args::ConfigArguments;
|
||||||
|
|
||||||
/// Resolve the relevant settings strategy and defaults for the current
|
/// 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
|
// `pyproject.toml` for _all_ configuration, and resolve paths relative to the
|
||||||
// current working directory. (This matches ESLint's behavior.)
|
// current working directory. (This matches ESLint's behavior.)
|
||||||
if let Some(pyproject) = config_arguments.config_file() {
|
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!(
|
debug!(
|
||||||
"Using user-specified configuration file at: {}",
|
"Using user-specified configuration file at: {}",
|
||||||
pyproject.display()
|
pyproject.display()
|
||||||
|
@ -61,7 +67,8 @@ pub fn resolve(
|
||||||
"Using configuration file (via parent) at: {}",
|
"Using configuration file (via parent) at: {}",
|
||||||
pyproject.display()
|
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(
|
return Ok(PyprojectConfig::new(
|
||||||
PyprojectDiscoveryStrategy::Hierarchical,
|
PyprojectDiscoveryStrategy::Hierarchical,
|
||||||
settings,
|
settings,
|
||||||
|
@ -74,11 +81,35 @@ pub fn resolve(
|
||||||
// end up the "closest" `pyproject.toml` file for every Python file later on, so
|
// end up the "closest" `pyproject.toml` file for every Python file later on, so
|
||||||
// these act as the "default" settings.)
|
// these act as the "default" settings.)
|
||||||
if let Some(pyproject) = pyproject::find_user_settings_toml() {
|
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!(
|
debug!(
|
||||||
"Using configuration file (via cwd) at: {}",
|
"Using configuration file (via cwd) at: {}",
|
||||||
pyproject.display()
|
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(
|
return Ok(PyprojectConfig::new(
|
||||||
PyprojectDiscoveryStrategy::Hierarchical,
|
PyprojectDiscoveryStrategy::Hierarchical,
|
||||||
settings,
|
settings,
|
||||||
|
@ -91,7 +122,24 @@ pub fn resolve(
|
||||||
// "closest" `pyproject.toml` file for every Python file later on, so these act
|
// "closest" `pyproject.toml` file for every Python file later on, so these act
|
||||||
// as the "default" settings.)
|
// as the "default" settings.)
|
||||||
debug!("Using Ruff 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)?;
|
let settings = config.into_settings(&path_dedot::CWD)?;
|
||||||
Ok(PyprojectConfig::new(
|
Ok(PyprojectConfig::new(
|
||||||
PyprojectDiscoveryStrategy::Hierarchical,
|
PyprojectDiscoveryStrategy::Hierarchical,
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -40,6 +40,7 @@ shellexpand = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
tracing-log = { workspace = true }
|
||||||
tracing-subscriber = { workspace = true, features = ["chrono"] }
|
tracing-subscriber = { workspace = true, features = ["chrono"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[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)
|
tracing::subscriber::set_global_default(subscriber)
|
||||||
.expect("should be able to 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.
|
/// 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::GlobPath;
|
||||||
use ruff_linter::{settings::types::FilePattern, settings::types::PreviewMode};
|
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::resolver::match_exclusion;
|
||||||
use ruff_workspace::Settings;
|
use ruff_workspace::Settings;
|
||||||
use ruff_workspace::{
|
use ruff_workspace::{
|
||||||
configuration::{Configuration, FormatConfiguration, LintConfiguration, RuleSelection},
|
configuration::{Configuration, FormatConfiguration, LintConfiguration, RuleSelection},
|
||||||
pyproject::{find_user_settings_toml, settings_toml},
|
pyproject::{find_user_settings_toml, settings_toml},
|
||||||
resolver::{ConfigurationTransformer, Relativity},
|
resolver::ConfigurationTransformer,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::session::settings::{
|
use crate::session::settings::{
|
||||||
|
@ -64,12 +65,36 @@ impl RuffSettings {
|
||||||
/// In the absence of a valid configuration file, it gracefully falls back to
|
/// In the absence of a valid configuration file, it gracefully falls back to
|
||||||
/// editor-only settings.
|
/// editor-only settings.
|
||||||
pub(crate) fn fallback(editor_settings: &ResolvedEditorSettings, root: &Path) -> RuffSettings {
|
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()
|
find_user_settings_toml()
|
||||||
.and_then(|user_settings| {
|
.and_then(|user_settings| {
|
||||||
|
tracing::debug!(
|
||||||
|
"Loading settings from user configuration file: `{}`",
|
||||||
|
user_settings.display()
|
||||||
|
);
|
||||||
ruff_workspace::resolver::resolve_root_settings(
|
ruff_workspace::resolver::resolve_root_settings(
|
||||||
&user_settings,
|
&user_settings,
|
||||||
Relativity::Cwd,
|
&FallbackTransformer {
|
||||||
&EditorConfigurationTransformer(editor_settings, root),
|
inner: EditorConfigurationTransformer(editor_settings, root),
|
||||||
|
},
|
||||||
|
ruff_workspace::resolver::ConfigurationOrigin::UserSettings,
|
||||||
)
|
)
|
||||||
.ok()
|
.ok()
|
||||||
.map(|settings| RuffSettings {
|
.map(|settings| RuffSettings {
|
||||||
|
@ -77,21 +102,45 @@ impl RuffSettings {
|
||||||
settings,
|
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
|
/// Constructs [`RuffSettings`] by merging the editor-defined settings with the
|
||||||
/// default configuration.
|
/// default configuration.
|
||||||
fn editor_only(editor_settings: &ResolvedEditorSettings, root: &Path) -> RuffSettings {
|
fn editor_only(editor_settings: &ResolvedEditorSettings, root: &Path) -> RuffSettings {
|
||||||
let settings = EditorConfigurationTransformer(editor_settings, root)
|
Self::with_editor_settings(editor_settings, root, Configuration::default())
|
||||||
.transform(Configuration::default())
|
.expect("editor configuration should merge successfully with default configuration")
|
||||||
.into_settings(root)
|
}
|
||||||
.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,
|
path: None,
|
||||||
settings,
|
settings,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,10 +189,11 @@ impl RuffSettingsIndex {
|
||||||
Ok(Some(pyproject)) => {
|
Ok(Some(pyproject)) => {
|
||||||
match ruff_workspace::resolver::resolve_root_settings(
|
match ruff_workspace::resolver::resolve_root_settings(
|
||||||
&pyproject,
|
&pyproject,
|
||||||
Relativity::Parent,
|
|
||||||
&EditorConfigurationTransformer(editor_settings, root),
|
&EditorConfigurationTransformer(editor_settings, root),
|
||||||
|
ruff_workspace::resolver::ConfigurationOrigin::Ancestor,
|
||||||
) {
|
) {
|
||||||
Ok(settings) => {
|
Ok(settings) => {
|
||||||
|
tracing::debug!("Loaded settings from: `{}`", pyproject.display());
|
||||||
respect_gitignore = Some(settings.file_resolver.respect_gitignore);
|
respect_gitignore = Some(settings.file_resolver.respect_gitignore);
|
||||||
|
|
||||||
index.insert(
|
index.insert(
|
||||||
|
@ -264,10 +314,15 @@ impl RuffSettingsIndex {
|
||||||
Ok(Some(pyproject)) => {
|
Ok(Some(pyproject)) => {
|
||||||
match ruff_workspace::resolver::resolve_root_settings(
|
match ruff_workspace::resolver::resolve_root_settings(
|
||||||
&pyproject,
|
&pyproject,
|
||||||
Relativity::Parent,
|
|
||||||
&EditorConfigurationTransformer(editor_settings, root),
|
&EditorConfigurationTransformer(editor_settings, root),
|
||||||
|
ruff_workspace::resolver::ConfigurationOrigin::Ancestor,
|
||||||
) {
|
) {
|
||||||
Ok(settings) => {
|
Ok(settings) => {
|
||||||
|
tracing::debug!(
|
||||||
|
"Loaded settings from: `{}` for `{}`",
|
||||||
|
pyproject.display(),
|
||||||
|
directory.display()
|
||||||
|
);
|
||||||
index.write().unwrap().insert(
|
index.write().unwrap().insert(
|
||||||
directory,
|
directory,
|
||||||
Arc::new(RuffSettings {
|
Arc::new(RuffSettings {
|
||||||
|
@ -437,8 +492,8 @@ impl ConfigurationTransformer for EditorConfigurationTransformer<'_> {
|
||||||
fn open_configuration_file(config_path: &Path) -> crate::Result<Configuration> {
|
fn open_configuration_file(config_path: &Path) -> crate::Result<Configuration> {
|
||||||
ruff_workspace::resolver::resolve_configuration(
|
ruff_workspace::resolver::resolve_configuration(
|
||||||
config_path,
|
config_path,
|
||||||
Relativity::Cwd,
|
|
||||||
&IdentityTransformer,
|
&IdentityTransformer,
|
||||||
|
ruff_workspace::resolver::ConfigurationOrigin::UserSpecified,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,18 +42,18 @@ impl Pyproject {
|
||||||
|
|
||||||
/// Parse a `ruff.toml` file.
|
/// Parse a `ruff.toml` file.
|
||||||
fn parse_ruff_toml<P: AsRef<Path>>(path: P) -> Result<Options> {
|
fn parse_ruff_toml<P: AsRef<Path>>(path: P) -> Result<Options> {
|
||||||
let contents = std::fs::read_to_string(path.as_ref())
|
let path = path.as_ref();
|
||||||
.with_context(|| format!("Failed to read {}", path.as_ref().display()))?;
|
let contents = std::fs::read_to_string(path)
|
||||||
toml::from_str(&contents)
|
.with_context(|| format!("Failed to read {}", path.display()))?;
|
||||||
.with_context(|| format!("Failed to parse {}", path.as_ref().display()))
|
toml::from_str(&contents).with_context(|| format!("Failed to parse {}", path.display()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a `pyproject.toml` file.
|
/// Parse a `pyproject.toml` file.
|
||||||
fn parse_pyproject_toml<P: AsRef<Path>>(path: P) -> Result<Pyproject> {
|
fn parse_pyproject_toml<P: AsRef<Path>>(path: P) -> Result<Pyproject> {
|
||||||
let contents = std::fs::read_to_string(path.as_ref())
|
let path = path.as_ref();
|
||||||
.with_context(|| format!("Failed to read {}", path.as_ref().display()))?;
|
let contents = std::fs::read_to_string(path)
|
||||||
toml::from_str(&contents)
|
.with_context(|| format!("Failed to read {}", path.display()))?;
|
||||||
.with_context(|| format!("Failed to parse {}", path.as_ref().display()))
|
toml::from_str(&contents).with_context(|| format!("Failed to parse {}", path.display()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return `true` if a `pyproject.toml` contains a `[tool.ruff]` section.
|
/// 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
|
/// Return the path to the `pyproject.toml` or `ruff.toml` file in a given
|
||||||
/// directory.
|
/// directory.
|
||||||
pub fn settings_toml<P: AsRef<Path>>(path: P) -> Result<Option<PathBuf>> {
|
pub fn settings_toml<P: AsRef<Path>>(path: P) -> Result<Option<PathBuf>> {
|
||||||
|
let path = path.as_ref();
|
||||||
// Check for `.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() {
|
if ruff_toml.is_file() {
|
||||||
return Ok(Some(ruff_toml));
|
return Ok(Some(ruff_toml));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for `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() {
|
if ruff_toml.is_file() {
|
||||||
return Ok(Some(ruff_toml));
|
return Ok(Some(ruff_toml));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for `pyproject.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)? {
|
if pyproject_toml.is_file() && ruff_enabled(&pyproject_toml)? {
|
||||||
return Ok(Some(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)
|
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
|
/// Find the path to the user-specific `pyproject.toml` or `ruff.toml`, if it
|
||||||
/// exists.
|
/// exists.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[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.
|
/// Load `Options` from a `pyproject.toml` or `ruff.toml` file.
|
||||||
pub(super) fn load_options<P: AsRef<Path>>(path: P) -> Result<Options> {
|
pub(super) fn load_options<P: AsRef<Path>>(
|
||||||
if path.as_ref().ends_with("pyproject.toml") {
|
path: P,
|
||||||
let pyproject = parse_pyproject_toml(&path)?;
|
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
|
let mut ruff = pyproject
|
||||||
.tool
|
.tool
|
||||||
.and_then(|tool| tool.ruff)
|
.and_then(|tool| tool.ruff)
|
||||||
|
@ -157,16 +173,55 @@ pub(super) fn load_options<P: AsRef<Path>>(path: P) -> Result<Options> {
|
||||||
}
|
}
|
||||||
Ok(ruff)
|
Ok(ruff)
|
||||||
} else {
|
} else {
|
||||||
let ruff = parse_ruff_toml(path);
|
let mut ruff = parse_ruff_toml(path);
|
||||||
if let Ok(ruff) = &ruff {
|
if let Ok(ref mut ruff) = ruff {
|
||||||
if ruff.target_version.is_none() {
|
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
|
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.
|
/// Infer the minimum supported [`PythonVersion`] from a `requires-python` specifier.
|
||||||
fn get_minimum_supported_version(requires_version: &VersionSpecifiers) -> Option<PythonVersion> {
|
fn get_minimum_supported_version(requires_version: &VersionSpecifiers) -> Option<PythonVersion> {
|
||||||
/// Truncate a version to its major and minor components.
|
/// 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)
|
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::settings_toml;
|
use crate::pyproject::{settings_toml, TargetVersionStrategy};
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
use crate::{pyproject, FileResolverSettings};
|
use crate::{pyproject, FileResolverSettings};
|
||||||
|
|
||||||
|
@ -301,9 +301,10 @@ pub trait ConfigurationTransformer {
|
||||||
// resolving the "default" configuration).
|
// resolving the "default" configuration).
|
||||||
pub fn resolve_configuration(
|
pub fn resolve_configuration(
|
||||||
pyproject: &Path,
|
pyproject: &Path,
|
||||||
relativity: Relativity,
|
|
||||||
transformer: &dyn ConfigurationTransformer,
|
transformer: &dyn ConfigurationTransformer,
|
||||||
|
origin: ConfigurationOrigin,
|
||||||
) -> Result<Configuration> {
|
) -> Result<Configuration> {
|
||||||
|
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(pyproject));
|
||||||
while let Some(path) = next {
|
while let Some(path) = next {
|
||||||
|
@ -319,7 +320,19 @@ pub fn resolve_configuration(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the current path.
|
// 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}`",
|
||||||
|
@ -368,10 +381,12 @@ pub fn resolve_configuration(
|
||||||
/// `pyproject.toml`.
|
/// `pyproject.toml`.
|
||||||
fn resolve_scoped_settings<'a>(
|
fn resolve_scoped_settings<'a>(
|
||||||
pyproject: &'a Path,
|
pyproject: &'a Path,
|
||||||
relativity: Relativity,
|
|
||||||
transformer: &dyn ConfigurationTransformer,
|
transformer: &dyn ConfigurationTransformer,
|
||||||
|
origin: ConfigurationOrigin,
|
||||||
) -> Result<(&'a Path, Settings)> {
|
) -> 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 project_root = relativity.resolve(pyproject);
|
||||||
let settings = configuration.into_settings(project_root)?;
|
let settings = configuration.into_settings(project_root)?;
|
||||||
Ok((project_root, settings))
|
Ok((project_root, settings))
|
||||||
|
@ -381,13 +396,37 @@ fn resolve_scoped_settings<'a>(
|
||||||
/// configuration with the given [`ConfigurationTransformer`].
|
/// configuration with the given [`ConfigurationTransformer`].
|
||||||
pub fn resolve_root_settings(
|
pub fn resolve_root_settings(
|
||||||
pyproject: &Path,
|
pyproject: &Path,
|
||||||
relativity: Relativity,
|
|
||||||
transformer: &dyn ConfigurationTransformer,
|
transformer: &dyn ConfigurationTransformer,
|
||||||
|
origin: ConfigurationOrigin,
|
||||||
) -> Result<Settings> {
|
) -> Result<Settings> {
|
||||||
let (_project_root, settings) = resolve_scoped_settings(pyproject, relativity, transformer)?;
|
let (_project_root, settings) = resolve_scoped_settings(pyproject, transformer, origin)?;
|
||||||
Ok(settings)
|
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.
|
/// Find all Python (`.py`, `.pyi` and `.ipynb` files) in a set of paths.
|
||||||
pub fn python_files_in_path<'a>(
|
pub fn python_files_in_path<'a>(
|
||||||
paths: &[PathBuf],
|
paths: &[PathBuf],
|
||||||
|
@ -411,8 +450,11 @@ pub fn python_files_in_path<'a>(
|
||||||
for ancestor in path.ancestors() {
|
for ancestor in path.ancestors() {
|
||||||
if seen.insert(ancestor) {
|
if seen.insert(ancestor) {
|
||||||
if let Some(pyproject) = settings_toml(ancestor)? {
|
if let Some(pyproject) = settings_toml(ancestor)? {
|
||||||
let (root, settings) =
|
let (root, settings) = resolve_scoped_settings(
|
||||||
resolve_scoped_settings(&pyproject, Relativity::Parent, transformer)?;
|
&pyproject,
|
||||||
|
transformer,
|
||||||
|
ConfigurationOrigin::Ancestor,
|
||||||
|
)?;
|
||||||
resolver.add(root, settings);
|
resolver.add(root, settings);
|
||||||
// We found the closest configuration.
|
// We found the closest configuration.
|
||||||
break;
|
break;
|
||||||
|
@ -564,8 +606,8 @@ impl ParallelVisitor for PythonFilesVisitor<'_, '_> {
|
||||||
match settings_toml(entry.path()) {
|
match settings_toml(entry.path()) {
|
||||||
Ok(Some(pyproject)) => match resolve_scoped_settings(
|
Ok(Some(pyproject)) => match resolve_scoped_settings(
|
||||||
&pyproject,
|
&pyproject,
|
||||||
Relativity::Parent,
|
|
||||||
self.transformer,
|
self.transformer,
|
||||||
|
ConfigurationOrigin::Ancestor,
|
||||||
) {
|
) {
|
||||||
Ok((root, settings)) => {
|
Ok((root, settings)) => {
|
||||||
self.global.resolver.write().unwrap().add(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() {
|
for ancestor in path.ancestors() {
|
||||||
if let Some(pyproject) = settings_toml(ancestor)? {
|
if let Some(pyproject) = settings_toml(ancestor)? {
|
||||||
let (root, settings) =
|
let (root, settings) =
|
||||||
resolve_scoped_settings(&pyproject, Relativity::Parent, transformer)?;
|
resolve_scoped_settings(&pyproject, transformer, ConfigurationOrigin::Unknown)?;
|
||||||
resolver.add(root, settings);
|
resolver.add(root, settings);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -883,7 +925,7 @@ mod tests {
|
||||||
use crate::pyproject::find_settings_toml;
|
use crate::pyproject::find_settings_toml;
|
||||||
use crate::resolver::{
|
use crate::resolver::{
|
||||||
is_file_excluded, match_exclusion, python_files_in_path, resolve_root_settings,
|
is_file_excluded, match_exclusion, python_files_in_path, resolve_root_settings,
|
||||||
ConfigurationTransformer, PyprojectConfig, PyprojectDiscoveryStrategy, Relativity,
|
ConfigurationOrigin, ConfigurationTransformer, PyprojectConfig, PyprojectDiscoveryStrategy,
|
||||||
ResolvedFile, Resolver,
|
ResolvedFile, Resolver,
|
||||||
};
|
};
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
|
@ -904,8 +946,8 @@ mod tests {
|
||||||
PyprojectDiscoveryStrategy::Hierarchical,
|
PyprojectDiscoveryStrategy::Hierarchical,
|
||||||
resolve_root_settings(
|
resolve_root_settings(
|
||||||
&find_settings_toml(&package_root)?.unwrap(),
|
&find_settings_toml(&package_root)?.unwrap(),
|
||||||
Relativity::Parent,
|
|
||||||
&NoOpTransformer,
|
&NoOpTransformer,
|
||||||
|
ConfigurationOrigin::Ancestor,
|
||||||
)?,
|
)?,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue