Add support for ruff.toml (#1378)

This commit is contained in:
Charlie Marsh 2022-12-25 21:55:07 -05:00 committed by GitHub
parent 28c45eb2a3
commit b0f30bef8f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 121 additions and 71 deletions

View file

@ -214,13 +214,6 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
# Assume Python 3.10. # Assume Python 3.10.
target-version = "py310" target-version = "py310"
[tool.ruff.flake8-import-conventions.aliases]
altair = "alt"
"matplotlib.pyplot" = "plt"
numpy = "np"
pandas = "pd"
seaborn = "sns"
[tool.ruff.mccabe] [tool.ruff.mccabe]
# Unlike Flake8, default to a complexity level of 10. # Unlike Flake8, default to a complexity level of 10.
max-complexity = 10 max-complexity = 10
@ -259,6 +252,27 @@ select = ["E", "F", "Q"]
docstring-quotes = "double" 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). For a full list of configurable options, see the [API reference](#reference).
Some common configuration settings can be provided via the command-line: Some common configuration settings can be provided via the command-line:
@ -279,7 +293,7 @@ Arguments:
Options: Options:
--config <CONFIG> --config <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 -v, --verbose
Enable verbose logging Enable verbose logging
-q, --quiet -q, --quiet
@ -385,6 +399,9 @@ extend = "../pyproject.toml"
line-length = 100 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 ### Python file discovery
When passed a path on the command-line, Ruff will automatically discover all Python files in that When passed a path on the command-line, Ruff will automatically discover all Python files in that

View file

@ -1,4 +1,3 @@
[tool.ruff]
extend = "../../pyproject.toml" extend = "../../pyproject.toml"
src = ["."] src = ["."]
# Enable I001, and re-enable F841, to test extension priority. # Enable I001, and re-enable F841, to test extension priority.

View file

@ -19,7 +19,8 @@ use crate::settings::types::{
pub struct Cli { pub struct Cli {
#[arg(required_unless_present_any = ["explain", "generate_shell_completion"])] #[arg(required_unless_present_any = ["explain", "generate_shell_completion"])]
pub files: Vec<PathBuf>, pub files: Vec<PathBuf>,
/// 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)] #[arg(long)]
pub config: Option<PathBuf>, pub config: Option<PathBuf>,
/// Enable verbose logging. /// Enable verbose logging.

View file

@ -89,10 +89,10 @@ pub mod visibility;
/// Load the relevant `Settings` for a given `Path`. /// Load the relevant `Settings` for a given `Path`.
fn resolve(path: &Path) -> Result<Settings> { fn resolve(path: &Path) -> Result<Settings> {
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`. // First priority: `pyproject.toml` in the current `Path`.
resolver::resolve_settings(&pyproject, &Relativity::Parent, None) 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`. // Second priority: user-specific `pyproject.toml`.
resolver::resolve_settings(&pyproject, &Relativity::Cwd, None) resolver::resolve_settings(&pyproject, &Relativity::Cwd, None)
} else { } else {

View file

@ -46,7 +46,7 @@ fn resolve(
// current working directory. (This matches ESLint's behavior.) // current working directory. (This matches ESLint's behavior.)
let settings = resolve_settings(pyproject, &Relativity::Cwd, Some(overrides))?; let settings = resolve_settings(pyproject, &Relativity::Cwd, Some(overrides))?;
Ok(PyprojectDiscovery::Fixed(settings)) Ok(PyprojectDiscovery::Fixed(settings))
} else if let Some(pyproject) = pyproject::find_pyproject_toml( } else if let Some(pyproject) = pyproject::find_settings_toml(
stdin_filename stdin_filename
.as_ref() .as_ref()
.unwrap_or(&path_dedot::CWD.as_path()), .unwrap_or(&path_dedot::CWD.as_path()),
@ -58,7 +58,7 @@ fn resolve(
// so these act as the "default" settings.) // so these act as the "default" settings.)
let settings = resolve_settings(&pyproject, &Relativity::Parent, Some(overrides))?; let settings = resolve_settings(&pyproject, &Relativity::Parent, Some(overrides))?;
Ok(PyprojectDiscovery::Hierarchical(settings)) 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 // Third priority: find a user-specific `pyproject.toml`, but resolve all paths
// relative the current working directory. (With `Strategy::Hierarchical`, we'll // relative the current working directory. (With `Strategy::Hierarchical`, we'll
// 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

View file

@ -14,7 +14,7 @@ use rustc_hash::FxHashSet;
use crate::cli::Overrides; use crate::cli::Overrides;
use crate::fs; use crate::fs;
use crate::settings::configuration::Configuration; use crate::settings::configuration::Configuration;
use crate::settings::pyproject::has_ruff_section; use crate::settings::pyproject::settings_toml;
use crate::settings::{pyproject, Settings}; use crate::settings::{pyproject, Settings};
/// The strategy used to discover Python files in the filesystem.. /// The strategy used to discover Python files in the filesystem..
@ -221,16 +221,13 @@ pub fn python_files_in_path(
let mut resolver = Resolver::default(); let mut resolver = Resolver::default();
for path in &paths { for path in &paths {
for ancestor in path.ancestors() { for ancestor in path.ancestors() {
let pyproject = ancestor.join("pyproject.toml"); if let Some(pyproject) = settings_toml(ancestor)? {
if pyproject.is_file() {
if has_ruff_section(&pyproject)? {
let (root, settings) = let (root, settings) =
resolve_scoped_settings(&pyproject, &Relativity::Parent, Some(overrides))?; resolve_scoped_settings(&pyproject, &Relativity::Parent, Some(overrides))?;
resolver.add(root, settings); resolver.add(root, settings);
} }
} }
} }
}
// Check if the paths themselves are excluded. // Check if the paths themselves are excluded.
if file_strategy.force_exclude { if file_strategy.force_exclude {
@ -267,12 +264,8 @@ pub fn python_files_in_path(
.file_type() .file_type()
.map_or(false, |file_type| file_type.is_dir()) .map_or(false, |file_type| file_type.is_dir())
{ {
let pyproject = entry.path().join("pyproject.toml"); match settings_toml(entry.path()) {
if pyproject.is_file() { Ok(Some(pyproject)) => match resolve_scoped_settings(
match has_ruff_section(&pyproject) {
Ok(false) => {}
Ok(true) => {
match resolve_scoped_settings(
&pyproject, &pyproject,
&Relativity::Parent, &Relativity::Parent,
Some(overrides), Some(overrides),
@ -284,8 +277,8 @@ pub fn python_files_in_path(
*error.lock().unwrap() = Err(err); *error.lock().unwrap() = Err(err);
return WalkState::Quit; return WalkState::Quit;
} }
} },
} Ok(None) => {}
Err(err) => { Err(err) => {
*error.lock().unwrap() = Err(err); *error.lock().unwrap() = Err(err);
return WalkState::Quit; return WalkState::Quit;
@ -293,7 +286,6 @@ pub fn python_files_in_path(
} }
} }
} }
}
// Respect our own exclusion behavior. // Respect our own exclusion behavior.
if let Ok(entry) = &result { if let Ok(entry) = &result {
@ -357,15 +349,12 @@ pub fn python_file_at_path(
// Search for `pyproject.toml` files in all parent directories. // Search for `pyproject.toml` files in all parent directories.
let mut resolver = Resolver::default(); let mut resolver = Resolver::default();
for ancestor in path.ancestors() { for ancestor in path.ancestors() {
let pyproject = ancestor.join("pyproject.toml"); if let Some(pyproject) = settings_toml(ancestor)? {
if pyproject.is_file() {
if has_ruff_section(&pyproject)? {
let (root, settings) = let (root, settings) =
resolve_scoped_settings(&pyproject, &Relativity::Parent, Some(overrides))?; resolve_scoped_settings(&pyproject, &Relativity::Parent, Some(overrides))?;
resolver.add(root, settings); resolver.add(root, settings);
} }
} }
}
// Check exclusions. // Check exclusions.
Ok(!is_file_excluded(&path, &resolver, pyproject_strategy)) Ok(!is_file_excluded(&path, &resolver, pyproject_strategy))

View file

@ -66,8 +66,8 @@ pub struct Configuration {
} }
impl Configuration { impl Configuration {
pub fn from_pyproject(pyproject: &Path, project_root: &Path) -> Result<Self> { pub fn from_toml(path: &Path, project_root: &Path) -> Result<Self> {
Self::from_options(load_options(pyproject)?, project_root) Self::from_options(load_options(path)?, project_root)
} }
pub fn from_options(options: Options, project_root: &Path) -> Result<Self> { pub fn from_options(options: Options, project_root: &Path) -> Result<Self> {

View file

@ -28,53 +28,97 @@ impl Pyproject {
} }
} }
/// Parse a `ruff.toml` file.
fn parse_ruff_toml<P: AsRef<Path>>(path: P) -> Result<Options> {
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<P: AsRef<Path>>(path: P) -> Result<Pyproject> { fn parse_pyproject_toml<P: AsRef<Path>>(path: P) -> Result<Pyproject> {
let contents = fs::read_file(path)?; let contents = fs::read_file(path)?;
toml::from_str(&contents).map_err(std::convert::Into::into) toml::from_str(&contents).map_err(std::convert::Into::into)
} }
/// Return `true` if a `pyproject.toml` contains a `[tool.ruff]` section. /// Return `true` if a `pyproject.toml` contains a `[tool.ruff]` section.
pub fn has_ruff_section<P: AsRef<Path>>(path: P) -> Result<bool> { pub fn ruff_enabled<P: AsRef<Path>>(path: P) -> Result<bool> {
let pyproject = parse_pyproject_toml(path)?; let pyproject = parse_pyproject_toml(path)?;
Ok(pyproject.tool.and_then(|tool| tool.ruff).is_some()) Ok(pyproject.tool.and_then(|tool| tool.ruff).is_some())
} }
/// Find the path to the `pyproject.toml` file, if such a file exists. /// Return the path to the `pyproject.toml` or `ruff.toml` file in a given
pub fn find_pyproject_toml<P: AsRef<Path>>(path: P) -> Result<Option<PathBuf>> { /// directory.
pub fn settings_toml<P: AsRef<Path>>(path: P) -> Result<Option<PathBuf>> {
// 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<P: AsRef<Path>>(path: P) -> Result<Option<PathBuf>> {
for directory in path.as_ref().ancestors() { for directory in path.as_ref().ancestors() {
let pyproject = directory.join("pyproject.toml"); if let Some(pyproject) = settings_toml(directory)? {
if pyproject.is_file() && has_ruff_section(&pyproject)? {
return Ok(Some(pyproject)); return Ok(Some(pyproject));
} }
} }
Ok(None) Ok(None)
} }
/// Find the path to the user-specific `pyproject.toml`, if it exists. /// Find the path to the user-specific `pyproject.toml` or `ruff.toml`, if it
pub fn find_user_pyproject_toml() -> Option<PathBuf> { /// exists.
pub fn find_user_settings_toml() -> Option<PathBuf> {
// 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()?; let mut path = dirs::config_dir()?;
path.push("ruff"); path.push("ruff");
path.push("pyproject.toml"); path.push("pyproject.toml");
if path.is_file() { if path.is_file() {
Some(path) return Some(path);
} else {
None
} }
None
} }
/// Load `Options` from a `pyproject.toml`. /// Load `Options` from a `pyproject.toml` or `ruff.toml` file.
pub fn load_options<P: AsRef<Path>>(pyproject: P) -> Result<Options> { pub fn load_options<P: AsRef<Path>>(path: P) -> Result<Options> {
Ok(parse_pyproject_toml(&pyproject) if path.as_ref().ends_with("ruff.toml") {
.map_err(|err| { parse_ruff_toml(path)
} else if path.as_ref().ends_with("pyproject.toml") {
let pyproject = parse_pyproject_toml(&path).map_err(|err| {
anyhow!( anyhow!(
"Failed to parse `{}`: {}", "Failed to parse `{}`: {}",
pyproject.as_ref().to_string_lossy(), path.as_ref().to_string_lossy(),
err err
) )
})? })?;
Ok(pyproject
.tool .tool
.and_then(|tool| tool.ruff) .and_then(|tool| tool.ruff)
.unwrap_or_default()) .unwrap_or_default())
} else {
Err(anyhow!(
"Unrecognized settings file: `{}`",
path.as_ref().to_string_lossy()
))
}
} }
#[cfg(test)] #[cfg(test)]
@ -89,7 +133,7 @@ mod tests {
use crate::flake8_quotes::settings::Quote; use crate::flake8_quotes::settings::Quote;
use crate::flake8_tidy_imports::settings::Strictness; use crate::flake8_tidy_imports::settings::Strictness;
use crate::settings::pyproject::{ 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::settings::types::PatternPrefixPair;
use crate::{ use crate::{
@ -399,14 +443,14 @@ other-attribute = 1
fn find_and_parse_pyproject_toml() -> Result<()> { fn find_and_parse_pyproject_toml() -> Result<()> {
let cwd = current_dir()?; let cwd = current_dir()?;
let pyproject = 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!( assert_eq!(
pyproject, pyproject,
cwd.join("resources/test/fixtures/pyproject.toml") cwd.join("resources/test/fixtures/pyproject.toml")
); );
let pyproject = parse_pyproject_toml(&pyproject)?; 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!( assert_eq!(
config, config,
Options { Options {