Files
ruff/src/pyproject.rs
2022-09-19 20:32:31 -06:00

286 lines
7.3 KiB
Rust

use std::ops::Deref;
use std::path::{Path, PathBuf};
use anyhow::Result;
use common_path::common_path_all;
use log::debug;
use path_absolutize::path_dedot;
use serde::Deserialize;
use crate::checks::CheckCode;
use crate::fs;
pub fn load_config(paths: &[PathBuf]) -> Config {
let project_root = find_project_root(paths);
match find_pyproject_toml(project_root.as_deref()) {
Some(path) => {
debug!("Found pyproject.toml at: {:?}", path);
match parse_pyproject_toml(&path) {
Ok(pyproject) => pyproject
.tool
.and_then(|tool| tool.ruff)
.unwrap_or_default(),
Err(e) => {
println!("Failed to load pyproject.toml: {:?}", e);
println!("Falling back to default configuration...");
Default::default()
}
}
}
None => Default::default(),
}
}
#[derive(Debug, PartialEq, Eq, Deserialize, Default)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Config {
pub line_length: Option<usize>,
pub exclude: Option<Vec<String>>,
pub extend_exclude: Option<Vec<String>>,
pub select: Option<Vec<CheckCode>>,
pub ignore: Option<Vec<CheckCode>>,
}
#[derive(Debug, PartialEq, Eq, Deserialize)]
struct Tools {
ruff: Option<Config>,
}
#[derive(Debug, PartialEq, Eq, Deserialize)]
struct PyProject {
tool: Option<Tools>,
}
fn parse_pyproject_toml(path: &Path) -> Result<PyProject> {
let contents = fs::read_file(path)?;
toml::from_str(&contents).map_err(|e| e.into())
}
fn find_pyproject_toml(path: Option<&Path>) -> Option<PathBuf> {
if let Some(path) = path {
let path_pyproject_toml = path.join("pyproject.toml");
if path_pyproject_toml.is_file() {
return Some(path_pyproject_toml);
}
}
find_user_pyproject_toml()
}
fn find_user_pyproject_toml() -> Option<PathBuf> {
let mut path = dirs::config_dir()?;
path.push("ruff");
path.push("pyproject.toml");
Some(path)
}
fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
let cwd = path_dedot::CWD.deref();
let absolute_sources: Vec<PathBuf> = sources.iter().map(|source| cwd.join(source)).collect();
if let Some(prefix) = common_path_all(absolute_sources.iter().map(PathBuf::as_path)) {
for directory in prefix.ancestors() {
if directory.join(".git").is_dir() {
return Some(directory.to_path_buf());
}
if directory.join(".hg").is_dir() {
return Some(directory.to_path_buf());
}
if directory.join("pyproject.toml").is_file() {
return Some(directory.to_path_buf());
}
}
}
None
}
#[cfg(test)]
mod tests {
use std::env::current_dir;
use std::path::PathBuf;
use anyhow::Result;
use crate::checks::CheckCode;
use crate::pyproject::{
find_project_root, find_pyproject_toml, parse_pyproject_toml, Config, PyProject, Tools,
};
#[test]
fn deserialize() -> Result<()> {
let pyproject: PyProject = toml::from_str(r#""#)?;
assert_eq!(pyproject.tool, None);
let pyproject: PyProject = toml::from_str(
r#"
[tool.black]
"#,
)?;
assert_eq!(pyproject.tool, Some(Tools { ruff: None }));
let pyproject: PyProject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
"#,
)?;
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
line_length: None,
exclude: None,
extend_exclude: None,
select: None,
ignore: None,
})
})
);
let pyproject: PyProject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
line-length = 79
"#,
)?;
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
line_length: Some(79),
exclude: None,
extend_exclude: None,
select: None,
ignore: None,
})
})
);
let pyproject: PyProject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
exclude = ["foo.py"]
"#,
)?;
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
line_length: None,
exclude: Some(vec!["foo.py".to_string()]),
extend_exclude: None,
select: None,
ignore: None,
})
})
);
let pyproject: PyProject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
select = ["E501"]
"#,
)?;
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
line_length: None,
exclude: None,
extend_exclude: None,
select: Some(vec![CheckCode::E501]),
ignore: None,
})
})
);
let pyproject: PyProject = toml::from_str(
r#"
[tool.black]
[tool.ruff]
ignore = ["E501"]
"#,
)?;
assert_eq!(
pyproject.tool,
Some(Tools {
ruff: Some(Config {
line_length: None,
exclude: None,
extend_exclude: None,
select: None,
ignore: Some(vec![CheckCode::E501]),
})
})
);
assert!(toml::from_str::<PyProject>(
r#"
[tool.black]
[tool.ruff]
line_length = 79
"#,
)
.is_err());
assert!(toml::from_str::<PyProject>(
r#"
[tool.black]
[tool.ruff]
select = ["E123"]
"#,
)
.is_err());
assert!(toml::from_str::<PyProject>(
r#"
[tool.black]
[tool.ruff]
line-length = 79
other-attribute = 1
"#,
)
.is_err());
Ok(())
}
#[test]
fn find_and_parse_pyproject_toml() -> Result<()> {
let cwd = current_dir().unwrap_or_else(|_| ".".into());
let project_root =
find_project_root(&[PathBuf::from("resources/test/fixtures/__init__.py")])
.expect("Unable to find project root.");
assert_eq!(project_root, cwd.join("resources/test/fixtures"));
let path =
find_pyproject_toml(Some(&project_root)).expect("Unable to find pyproject.toml.");
assert_eq!(path, cwd.join("resources/test/fixtures/pyproject.toml"));
let pyproject = parse_pyproject_toml(&path)?;
let config = pyproject
.tool
.and_then(|tool| tool.ruff)
.expect("Unable to find tool.ruff.");
assert_eq!(
config,
Config {
line_length: Some(88),
exclude: None,
extend_exclude: Some(vec![
"excluded.py".to_string(),
"migrations".to_string(),
"resources/test/fixtures/directory/also_excluded.py".to_string(),
]),
select: None,
ignore: None,
}
);
Ok(())
}
}