From b0f30bef8fa8db36e7f44bb65a229c29e28d3379 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 25 Dec 2022 21:55:07 -0500 Subject: [PATCH] Add support for `ruff.toml` (#1378) --- README.md | 33 +++++-- .../docs/{pyproject.toml => ruff.toml} | 1 - src/cli.rs | 3 +- src/lib.rs | 4 +- src/main.rs | 4 +- src/resolver.rs | 55 +++++------- src/settings/configuration.rs | 4 +- src/settings/pyproject.rs | 88 ++++++++++++++----- 8 files changed, 121 insertions(+), 71 deletions(-) rename resources/test/project/examples/docs/{pyproject.toml => ruff.toml} (94%) diff --git a/README.md b/README.md index eb51614cb7..21e14da2b5 100644 --- a/README.md +++ b/README.md @@ -214,13 +214,6 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" # Assume Python 3.10. target-version = "py310" -[tool.ruff.flake8-import-conventions.aliases] -altair = "alt" -"matplotlib.pyplot" = "plt" -numpy = "np" -pandas = "pd" -seaborn = "sns" - [tool.ruff.mccabe] # Unlike Flake8, default to a complexity level of 10. max-complexity = 10 @@ -259,6 +252,27 @@ select = ["E", "F", "Q"] docstring-quotes = "double" ``` +As an alternative to `pyproject.toml`, Ruff will also respect a `ruff.toml` file, which implements +an equivalent schema (though the `[tool.ruff]` hierarchy can be omitted). For example, the above +`pyproject.toml` described above would be represented via the following `ruff.toml`: + +```toml +# Enable Pyflakes and pycodestyle rules. +select = ["E", "F"] + +# Never enforce `E501` (line length violations). +ignore = ["E501"] + +# Always autofix, but never try to fix `F401` (unused imports). +fix = true +unfixable = ["F401"] + +# Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`. +[per-file-ignores] +"__init__.py" = ["E402"] +"path/to/file.py" = ["E402"] +``` + For a full list of configurable options, see the [API reference](#reference). Some common configuration settings can be provided via the command-line: @@ -279,7 +293,7 @@ Arguments: Options: --config - Path to the `pyproject.toml` file to use for configuration + Path to the `pyproject.toml` or `ruff.toml` file to use for configuration -v, --verbose Enable verbose logging -q, --quiet @@ -385,6 +399,9 @@ extend = "../pyproject.toml" line-length = 100 ``` +All of the above rules apply equivalently to `ruff.toml` files. If Ruff detects both a `ruff.toml` +and `pyproject.toml` file, it will defer to the `ruff.toml`. + ### Python file discovery When passed a path on the command-line, Ruff will automatically discover all Python files in that diff --git a/resources/test/project/examples/docs/pyproject.toml b/resources/test/project/examples/docs/ruff.toml similarity index 94% rename from resources/test/project/examples/docs/pyproject.toml rename to resources/test/project/examples/docs/ruff.toml index 92f0b71d91..01b82cd542 100644 --- a/resources/test/project/examples/docs/pyproject.toml +++ b/resources/test/project/examples/docs/ruff.toml @@ -1,4 +1,3 @@ -[tool.ruff] extend = "../../pyproject.toml" src = ["."] # Enable I001, and re-enable F841, to test extension priority. diff --git a/src/cli.rs b/src/cli.rs index 7f518d7f83..df0e57b701 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -19,7 +19,8 @@ use crate::settings::types::{ pub struct Cli { #[arg(required_unless_present_any = ["explain", "generate_shell_completion"])] pub files: Vec, - /// Path to the `pyproject.toml` file to use for configuration. + /// Path to the `pyproject.toml` or `ruff.toml` file to use for + /// configuration. #[arg(long)] pub config: Option, /// Enable verbose logging. diff --git a/src/lib.rs b/src/lib.rs index b080ffcaec..79f7c3212c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,10 +89,10 @@ pub mod visibility; /// Load the relevant `Settings` for a given `Path`. fn resolve(path: &Path) -> Result { - if let Some(pyproject) = pyproject::find_pyproject_toml(path)? { + if let Some(pyproject) = pyproject::find_settings_toml(path)? { // First priority: `pyproject.toml` in the current `Path`. resolver::resolve_settings(&pyproject, &Relativity::Parent, None) - } else if let Some(pyproject) = pyproject::find_user_pyproject_toml() { + } else if let Some(pyproject) = pyproject::find_user_settings_toml() { // Second priority: user-specific `pyproject.toml`. resolver::resolve_settings(&pyproject, &Relativity::Cwd, None) } else { diff --git a/src/main.rs b/src/main.rs index 2947820375..a8f6fe9aa6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,7 +46,7 @@ fn resolve( // current working directory. (This matches ESLint's behavior.) let settings = resolve_settings(pyproject, &Relativity::Cwd, Some(overrides))?; Ok(PyprojectDiscovery::Fixed(settings)) - } else if let Some(pyproject) = pyproject::find_pyproject_toml( + } else if let Some(pyproject) = pyproject::find_settings_toml( stdin_filename .as_ref() .unwrap_or(&path_dedot::CWD.as_path()), @@ -58,7 +58,7 @@ fn resolve( // so these act as the "default" settings.) let settings = resolve_settings(&pyproject, &Relativity::Parent, Some(overrides))?; Ok(PyprojectDiscovery::Hierarchical(settings)) - } else if let Some(pyproject) = pyproject::find_user_pyproject_toml() { + } else if let Some(pyproject) = pyproject::find_user_settings_toml() { // Third priority: find a user-specific `pyproject.toml`, but resolve all paths // relative the current working directory. (With `Strategy::Hierarchical`, we'll // end up the "closest" `pyproject.toml` file for every Python file later on, so diff --git a/src/resolver.rs b/src/resolver.rs index d3da930e59..5fb7bc65a7 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -14,7 +14,7 @@ use rustc_hash::FxHashSet; use crate::cli::Overrides; use crate::fs; use crate::settings::configuration::Configuration; -use crate::settings::pyproject::has_ruff_section; +use crate::settings::pyproject::settings_toml; use crate::settings::{pyproject, Settings}; /// The strategy used to discover Python files in the filesystem.. @@ -221,13 +221,10 @@ pub fn python_files_in_path( let mut resolver = Resolver::default(); for path in &paths { for ancestor in path.ancestors() { - let pyproject = ancestor.join("pyproject.toml"); - if pyproject.is_file() { - if has_ruff_section(&pyproject)? { - let (root, settings) = - resolve_scoped_settings(&pyproject, &Relativity::Parent, Some(overrides))?; - resolver.add(root, settings); - } + if let Some(pyproject) = settings_toml(ancestor)? { + let (root, settings) = + resolve_scoped_settings(&pyproject, &Relativity::Parent, Some(overrides))?; + resolver.add(root, settings); } } } @@ -267,29 +264,24 @@ pub fn python_files_in_path( .file_type() .map_or(false, |file_type| file_type.is_dir()) { - let pyproject = entry.path().join("pyproject.toml"); - if pyproject.is_file() { - match has_ruff_section(&pyproject) { - Ok(false) => {} - Ok(true) => { - match resolve_scoped_settings( - &pyproject, - &Relativity::Parent, - Some(overrides), - ) { - Ok((root, settings)) => { - resolver.write().unwrap().add(root, settings); - } - Err(err) => { - *error.lock().unwrap() = Err(err); - return WalkState::Quit; - } - } + match settings_toml(entry.path()) { + Ok(Some(pyproject)) => match resolve_scoped_settings( + &pyproject, + &Relativity::Parent, + Some(overrides), + ) { + Ok((root, settings)) => { + resolver.write().unwrap().add(root, settings); } Err(err) => { *error.lock().unwrap() = Err(err); return WalkState::Quit; } + }, + Ok(None) => {} + Err(err) => { + *error.lock().unwrap() = Err(err); + return WalkState::Quit; } } } @@ -357,13 +349,10 @@ pub fn python_file_at_path( // Search for `pyproject.toml` files in all parent directories. let mut resolver = Resolver::default(); for ancestor in path.ancestors() { - let pyproject = ancestor.join("pyproject.toml"); - if pyproject.is_file() { - if has_ruff_section(&pyproject)? { - let (root, settings) = - resolve_scoped_settings(&pyproject, &Relativity::Parent, Some(overrides))?; - resolver.add(root, settings); - } + if let Some(pyproject) = settings_toml(ancestor)? { + let (root, settings) = + resolve_scoped_settings(&pyproject, &Relativity::Parent, Some(overrides))?; + resolver.add(root, settings); } } diff --git a/src/settings/configuration.rs b/src/settings/configuration.rs index 090215633a..81086b389e 100644 --- a/src/settings/configuration.rs +++ b/src/settings/configuration.rs @@ -66,8 +66,8 @@ pub struct Configuration { } impl Configuration { - pub fn from_pyproject(pyproject: &Path, project_root: &Path) -> Result { - Self::from_options(load_options(pyproject)?, project_root) + pub fn from_toml(path: &Path, project_root: &Path) -> Result { + Self::from_options(load_options(path)?, project_root) } pub fn from_options(options: Options, project_root: &Path) -> Result { diff --git a/src/settings/pyproject.rs b/src/settings/pyproject.rs index e487783847..403478ca08 100644 --- a/src/settings/pyproject.rs +++ b/src/settings/pyproject.rs @@ -28,53 +28,97 @@ impl Pyproject { } } +/// Parse a `ruff.toml` file. +fn parse_ruff_toml>(path: P) -> Result { + let contents = fs::read_file(path)?; + toml::from_str(&contents).map_err(std::convert::Into::into) +} + +/// Parse a `pyproject.toml` file. fn parse_pyproject_toml>(path: P) -> Result { let contents = fs::read_file(path)?; toml::from_str(&contents).map_err(std::convert::Into::into) } /// Return `true` if a `pyproject.toml` contains a `[tool.ruff]` section. -pub fn has_ruff_section>(path: P) -> Result { +pub fn ruff_enabled>(path: P) -> Result { let pyproject = parse_pyproject_toml(path)?; Ok(pyproject.tool.and_then(|tool| tool.ruff).is_some()) } -/// Find the path to the `pyproject.toml` file, if such a file exists. -pub fn find_pyproject_toml>(path: P) -> Result> { +/// Return the path to the `pyproject.toml` or `ruff.toml` file in a given +/// directory. +pub fn settings_toml>(path: P) -> Result> { + // Check for `ruff.toml`. + let ruff_toml = path.as_ref().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"); + if pyproject_toml.is_file() && ruff_enabled(&pyproject_toml)? { + return Ok(Some(pyproject_toml)); + } + + Ok(None) +} + +/// Find the path to the `pyproject.toml` or `ruff.toml` file, if such a file +/// exists. +pub fn find_settings_toml>(path: P) -> Result> { for directory in path.as_ref().ancestors() { - let pyproject = directory.join("pyproject.toml"); - if pyproject.is_file() && has_ruff_section(&pyproject)? { + if let Some(pyproject) = settings_toml(directory)? { return Ok(Some(pyproject)); } } Ok(None) } -/// Find the path to the user-specific `pyproject.toml`, if it exists. -pub fn find_user_pyproject_toml() -> Option { +/// Find the path to the user-specific `pyproject.toml` or `ruff.toml`, if it +/// exists. +pub fn find_user_settings_toml() -> Option { + // Search for a user-specific `ruff.toml`. + let mut path = dirs::config_dir()?; + path.push("ruff"); + path.push("ruff.toml"); + if path.is_file() { + return Some(path); + } + + // Search for a user-specific `pyproject.toml`. let mut path = dirs::config_dir()?; path.push("ruff"); path.push("pyproject.toml"); if path.is_file() { - Some(path) - } else { - None + return Some(path); } + + None } -/// Load `Options` from a `pyproject.toml`. -pub fn load_options>(pyproject: P) -> Result { - Ok(parse_pyproject_toml(&pyproject) - .map_err(|err| { +/// Load `Options` from a `pyproject.toml` or `ruff.toml` file. +pub fn load_options>(path: P) -> Result { + if path.as_ref().ends_with("ruff.toml") { + parse_ruff_toml(path) + } else if path.as_ref().ends_with("pyproject.toml") { + let pyproject = parse_pyproject_toml(&path).map_err(|err| { anyhow!( "Failed to parse `{}`: {}", - pyproject.as_ref().to_string_lossy(), + path.as_ref().to_string_lossy(), err ) - })? - .tool - .and_then(|tool| tool.ruff) - .unwrap_or_default()) + })?; + Ok(pyproject + .tool + .and_then(|tool| tool.ruff) + .unwrap_or_default()) + } else { + Err(anyhow!( + "Unrecognized settings file: `{}`", + path.as_ref().to_string_lossy() + )) + } } #[cfg(test)] @@ -89,7 +133,7 @@ mod tests { use crate::flake8_quotes::settings::Quote; use crate::flake8_tidy_imports::settings::Strictness; use crate::settings::pyproject::{ - find_pyproject_toml, parse_pyproject_toml, Options, Pyproject, Tools, + find_settings_toml, parse_pyproject_toml, Options, Pyproject, Tools, }; use crate::settings::types::PatternPrefixPair; use crate::{ @@ -399,14 +443,14 @@ other-attribute = 1 fn find_and_parse_pyproject_toml() -> Result<()> { let cwd = current_dir()?; let pyproject = - find_pyproject_toml(cwd.join("resources/test/fixtures/__init__.py"))?.unwrap(); + find_settings_toml(cwd.join("resources/test/fixtures/__init__.py"))?.unwrap(); assert_eq!( pyproject, cwd.join("resources/test/fixtures/pyproject.toml") ); let pyproject = parse_pyproject_toml(&pyproject)?; - let config = pyproject.tool.and_then(|tool| tool.ruff).unwrap(); + let config = pyproject.tool.unwrap().ruff.unwrap(); assert_eq!( config, Options {