diff --git a/README.md b/README.md index d1e7449343..d10ee26e67 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ Exclusions are based on globs, and can be either: `foo_*.py` ). - Relative patterns, like `directory/foo.py` (to exclude that specific file) or `directory/*.py` (to exclude any Python files in `directory`). Note that these paths are relative to the - directory from which you execute `ruff`, and _not_ the directory of the `pyproject.toml`. + project root (e.g., the directory containing your `pyproject.toml`). ### Compatibility with Black diff --git a/resources/test/fixtures/pyproject.toml b/resources/test/fixtures/pyproject.toml index 44951d06fb..be686c5767 100644 --- a/resources/test/fixtures/pyproject.toml +++ b/resources/test/fixtures/pyproject.toml @@ -3,5 +3,5 @@ line-length = 88 extend-exclude = [ "excluded.py", "migrations", - "resources/test/fixtures/directory/also_excluded.py", + "directory/also_excluded.py", ] diff --git a/src/fs.rs b/src/fs.rs index 6910c1c891..72ba732ca5 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -112,6 +112,7 @@ pub fn iter_python_files<'a>( }) } +/// Convert any path to an absolute path (based on the current working directory). pub fn normalize_path(path: &Path) -> PathBuf { if path == Path::new(".") || path == Path::new("..") { return path.to_path_buf(); @@ -122,6 +123,18 @@ pub fn normalize_path(path: &Path) -> PathBuf { path.to_path_buf() } +/// Convert any path to an absolute path (based on the specified project root). +pub fn normalize_path_to(path: &Path, project_root: &Path) -> PathBuf { + if path == Path::new(".") || path == Path::new("..") { + return path.to_path_buf(); + } + if let Ok(path) = path.absolutize_from(project_root) { + return path.to_path_buf(); + } + path.to_path_buf() +} + +/// Convert an absolute path to be relative to the current working directory. pub fn relativize_path(path: &Path) -> Cow { if let Ok(path) = path.strip_prefix(path_dedot::CWD.deref()) { return path.to_string_lossy(); @@ -129,6 +142,7 @@ pub fn relativize_path(path: &Path) -> Cow { path.to_string_lossy() } +/// Read a file's contents from disk. pub fn read_file(path: &Path) -> Result { let file = File::open(path)?; let mut buf_reader = BufReader::new(file); @@ -164,38 +178,69 @@ mod tests { #[test] fn exclusions() -> Result<()> { - let exclude = vec![FilePattern::from_user("foo")]; - let path = Path::new("foo").absolutize().unwrap(); + let project_root = Path::new("/tmp/"); + + let path = Path::new("foo").absolutize_from(project_root).unwrap(); + let exclude = vec![FilePattern::from_user( + "foo", + &Some(project_root.to_path_buf()), + )]; let (file_path, file_basename) = extract_path_names(&path)?; assert!(is_excluded(file_path, file_basename, &exclude)); - let exclude = vec![FilePattern::from_user("bar")]; - let path = Path::new("bar").absolutize().unwrap(); + let path = Path::new("foo/bar").absolutize_from(project_root).unwrap(); + let exclude = vec![FilePattern::from_user( + "bar", + &Some(project_root.to_path_buf()), + )]; let (file_path, file_basename) = extract_path_names(&path)?; assert!(is_excluded(file_path, file_basename, &exclude)); - let exclude = vec![FilePattern::from_user("baz.py")]; - let path = Path::new("baz.py").absolutize().unwrap(); + let path = Path::new("foo/bar/baz.py") + .absolutize_from(project_root) + .unwrap(); + let exclude = vec![FilePattern::from_user( + "baz.py", + &Some(project_root.to_path_buf()), + )]; let (file_path, file_basename) = extract_path_names(&path)?; assert!(is_excluded(file_path, file_basename, &exclude)); - let exclude = vec![FilePattern::from_user("foo/bar")]; - let path = Path::new("foo/bar").absolutize().unwrap(); + let path = Path::new("foo/bar").absolutize_from(project_root).unwrap(); + let exclude = vec![FilePattern::from_user( + "foo/bar", + &Some(project_root.to_path_buf()), + )]; let (file_path, file_basename) = extract_path_names(&path)?; assert!(is_excluded(file_path, file_basename, &exclude)); - let exclude = vec![FilePattern::from_user("foo/bar/baz.py")]; - let path = Path::new("foo/bar/baz.py").absolutize().unwrap(); + let path = Path::new("foo/bar/baz.py") + .absolutize_from(project_root) + .unwrap(); + let exclude = vec![FilePattern::from_user( + "foo/bar/baz.py", + &Some(project_root.to_path_buf()), + )]; let (file_path, file_basename) = extract_path_names(&path)?; assert!(is_excluded(file_path, file_basename, &exclude)); - let exclude = vec![FilePattern::from_user("foo/bar/*.py")]; - let path = Path::new("foo/bar/*.py").absolutize().unwrap(); + let path = Path::new("foo/bar/baz.py") + .absolutize_from(project_root) + .unwrap(); + let exclude = vec![FilePattern::from_user( + "foo/bar/*.py", + &Some(project_root.to_path_buf()), + )]; let (file_path, file_basename) = extract_path_names(&path)?; assert!(is_excluded(file_path, file_basename, &exclude)); - let exclude = vec![FilePattern::from_user("baz")]; - let path = Path::new("foo/bar/baz.py").absolutize().unwrap(); + let path = Path::new("foo/bar/baz.py") + .absolutize_from(project_root) + .unwrap(); + let exclude = vec![FilePattern::from_user( + "baz", + &Some(project_root.to_path_buf()), + )]; let (file_path, file_basename) = extract_path_names(&path)?; assert!(!is_excluded(file_path, file_basename, &exclude)); diff --git a/src/lib.rs b/src/lib.rs index 6f84021edb..22b5466d00 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,6 @@ pub mod linter; pub mod logging; pub mod message; pub mod printer; -mod pyproject; +pub mod pyproject; mod python; pub mod settings; diff --git a/src/main.rs b/src/main.rs index c3619d1053..c862617741 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,8 +21,8 @@ use ::ruff::linter::lint_path; use ::ruff::logging::set_up_logging; use ::ruff::message::Message; use ::ruff::printer::{Printer, SerializationFormat}; -use ::ruff::settings::FilePattern; -use ::ruff::settings::Settings; +use ::ruff::pyproject; +use ::ruff::settings::{FilePattern, Settings}; use ::ruff::tell_user; const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME"); @@ -155,9 +155,20 @@ fn inner_main() -> Result { set_up_logging(cli.verbose)?; - let mut settings = Settings::from_paths(&cli.files); - let mut printer = Printer::new(cli.format); + // Find the project root and pyproject.toml. + let project_root = pyproject::find_project_root(&cli.files); + match &project_root { + Some(path) => debug!("Found project root at: {:?}", path), + None => debug!("Unable to identify project root; assuming current directory..."), + }; + let pyproject = pyproject::find_pyproject_toml(&project_root); + match &pyproject { + Some(path) => debug!("Found pyproject.toml at: {:?}", path), + None => debug!("Unable to find pyproject.toml; using default settings..."), + }; + // Parse the settings from the pyproject.toml and command-line arguments. + let mut settings = Settings::from_pyproject(&pyproject, &project_root); if !cli.select.is_empty() { settings.select(cli.select); } @@ -168,19 +179,20 @@ fn inner_main() -> Result { settings.exclude = cli .exclude .iter() - .map(|path| FilePattern::from_user(path)) + .map(|path| FilePattern::from_user(path, &project_root)) .collect(); } if !cli.extend_exclude.is_empty() { settings.extend_exclude = cli .extend_exclude .iter() - .map(|path| FilePattern::from_user(path)) + .map(|path| FilePattern::from_user(path, &project_root)) .collect(); } cache::init()?; + let mut printer = Printer::new(cli.format); if cli.watch { if cli.fix { println!("Warning: --fix is not enabled in watch mode."); diff --git a/src/pyproject.rs b/src/pyproject.rs index b35f4449e4..8e65e04a65 100644 --- a/src/pyproject.rs +++ b/src/pyproject.rs @@ -3,31 +3,30 @@ 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() - } +pub fn load_config(pyproject: &Option) -> Config { + match pyproject { + Some(pyproject) => match parse_pyproject_toml(pyproject) { + 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 => { + println!("No pyproject.toml found."); + println!("Falling back to default configuration..."); + Default::default() } - None => Default::default(), } } @@ -56,7 +55,7 @@ fn parse_pyproject_toml(path: &Path) -> Result { toml::from_str(&contents).map_err(|e| e.into()) } -fn find_pyproject_toml(path: Option<&Path>) -> Option { +pub fn find_pyproject_toml(path: &Option) -> Option { if let Some(path) = path { let path_pyproject_toml = path.join("pyproject.toml"); if path_pyproject_toml.is_file() { @@ -71,10 +70,14 @@ fn find_user_pyproject_toml() -> Option { let mut path = dirs::config_dir()?; path.push("ruff"); path.push("pyproject.toml"); - Some(path) + if path.is_file() { + Some(path) + } else { + None + } } -fn find_project_root(sources: &[PathBuf]) -> Option { +pub fn find_project_root(sources: &[PathBuf]) -> Option { let cwd = path_dedot::CWD.deref(); let absolute_sources: Vec = sources.iter().map(|source| cwd.join(source)).collect(); if let Some(prefix) = common_path_all(absolute_sources.iter().map(PathBuf::as_path)) { @@ -250,14 +253,14 @@ other-attribute = 1 #[test] fn find_and_parse_pyproject_toml() -> Result<()> { - let cwd = current_dir().unwrap_or_else(|_| ".".into()); + let cwd = current_dir()?; 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."); + 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)?; @@ -273,7 +276,7 @@ other-attribute = 1 extend_exclude: Some(vec![ "excluded.py".to_string(), "migrations".to_string(), - "resources/test/fixtures/directory/also_excluded.py".to_string(), + "directory/also_excluded.py".to_string(), ]), select: None, ignore: None, diff --git a/src/settings.rs b/src/settings.rs index 03a586b745..b704a07cfb 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -16,9 +16,14 @@ pub enum FilePattern { } impl FilePattern { - pub fn from_user(pattern: &str) -> Self { - let absolute = Pattern::new(&fs::normalize_path(Path::new(pattern)).to_string_lossy()) - .expect("Invalid pattern."); + pub fn from_user(pattern: &str, project_root: &Option) -> Self { + let path = Path::new(pattern); + let absolute_path = match project_root { + Some(project_root) => fs::normalize_path_to(path, project_root), + None => fs::normalize_path(path), + }; + + let absolute = Pattern::new(&absolute_path.to_string_lossy()).expect("Invalid pattern."); let basename = if !pattern.contains(std::path::MAIN_SEPARATOR) { Some(Pattern::new(pattern).expect("Invalid pattern.")) } else { @@ -71,8 +76,8 @@ static DEFAULT_EXCLUDE: Lazy> = Lazy::new(|| { }); impl Settings { - pub fn from_paths(paths: &[PathBuf]) -> Self { - let config = load_config(paths); + pub fn from_pyproject(path: &Option, project_root: &Option) -> Self { + let config = load_config(path); let mut settings = Settings { line_length: config.line_length.unwrap_or(88), exclude: config @@ -80,7 +85,7 @@ impl Settings { .map(|paths| { paths .iter() - .map(|path| FilePattern::from_user(path)) + .map(|path| FilePattern::from_user(path, project_root)) .collect() }) .unwrap_or_else(|| DEFAULT_EXCLUDE.clone()), @@ -89,7 +94,7 @@ impl Settings { .map(|paths| { paths .iter() - .map(|path| FilePattern::from_user(path)) + .map(|path| FilePattern::from_user(path, project_root)) .collect() }) .unwrap_or_default(),