Treat relative excludes as relative to project root (#228)

This commit is contained in:
Charlie Marsh 2022-09-19 20:45:02 -06:00 committed by GitHub
parent a0b50d7ebc
commit fa0954fe47
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 118 additions and 53 deletions

View File

@ -134,7 +134,7 @@ Exclusions are based on globs, and can be either:
`foo_*.py` ). `foo_*.py` ).
- Relative patterns, like `directory/foo.py` (to exclude that specific file) or `directory/*.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 (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 ### Compatibility with Black

View File

@ -3,5 +3,5 @@ line-length = 88
extend-exclude = [ extend-exclude = [
"excluded.py", "excluded.py",
"migrations", "migrations",
"resources/test/fixtures/directory/also_excluded.py", "directory/also_excluded.py",
] ]

View File

@ -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 { pub fn normalize_path(path: &Path) -> PathBuf {
if path == Path::new(".") || path == Path::new("..") { if path == Path::new(".") || path == Path::new("..") {
return path.to_path_buf(); return path.to_path_buf();
@ -122,6 +123,18 @@ pub fn normalize_path(path: &Path) -> PathBuf {
path.to_path_buf() 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<str> { pub fn relativize_path(path: &Path) -> Cow<str> {
if let Ok(path) = path.strip_prefix(path_dedot::CWD.deref()) { if let Ok(path) = path.strip_prefix(path_dedot::CWD.deref()) {
return path.to_string_lossy(); return path.to_string_lossy();
@ -129,6 +142,7 @@ pub fn relativize_path(path: &Path) -> Cow<str> {
path.to_string_lossy() path.to_string_lossy()
} }
/// Read a file's contents from disk.
pub fn read_file(path: &Path) -> Result<String> { pub fn read_file(path: &Path) -> Result<String> {
let file = File::open(path)?; let file = File::open(path)?;
let mut buf_reader = BufReader::new(file); let mut buf_reader = BufReader::new(file);
@ -164,38 +178,69 @@ mod tests {
#[test] #[test]
fn exclusions() -> Result<()> { fn exclusions() -> Result<()> {
let exclude = vec![FilePattern::from_user("foo")]; let project_root = Path::new("/tmp/");
let path = Path::new("foo").absolutize().unwrap();
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)?; let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude)); assert!(is_excluded(file_path, file_basename, &exclude));
let exclude = vec![FilePattern::from_user("bar")]; let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
let path = Path::new("bar").absolutize().unwrap(); let exclude = vec![FilePattern::from_user(
"bar",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?; let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude)); assert!(is_excluded(file_path, file_basename, &exclude));
let exclude = vec![FilePattern::from_user("baz.py")]; let path = Path::new("foo/bar/baz.py")
let path = Path::new("baz.py").absolutize().unwrap(); .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)?; let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude)); assert!(is_excluded(file_path, file_basename, &exclude));
let exclude = vec![FilePattern::from_user("foo/bar")]; let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
let path = Path::new("foo/bar").absolutize().unwrap(); let exclude = vec![FilePattern::from_user(
"foo/bar",
&Some(project_root.to_path_buf()),
)];
let (file_path, file_basename) = extract_path_names(&path)?; let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude)); 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")
let path = Path::new("foo/bar/baz.py").absolutize().unwrap(); .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)?; let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude)); assert!(is_excluded(file_path, file_basename, &exclude));
let exclude = vec![FilePattern::from_user("foo/bar/*.py")]; let path = Path::new("foo/bar/baz.py")
let path = Path::new("foo/bar/*.py").absolutize().unwrap(); .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)?; let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, &exclude)); assert!(is_excluded(file_path, file_basename, &exclude));
let exclude = vec![FilePattern::from_user("baz")]; let path = Path::new("foo/bar/baz.py")
let path = Path::new("foo/bar/baz.py").absolutize().unwrap(); .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)?; let (file_path, file_basename) = extract_path_names(&path)?;
assert!(!is_excluded(file_path, file_basename, &exclude)); assert!(!is_excluded(file_path, file_basename, &exclude));

View File

@ -11,6 +11,6 @@ pub mod linter;
pub mod logging; pub mod logging;
pub mod message; pub mod message;
pub mod printer; pub mod printer;
mod pyproject; pub mod pyproject;
mod python; mod python;
pub mod settings; pub mod settings;

View File

@ -21,8 +21,8 @@ use ::ruff::linter::lint_path;
use ::ruff::logging::set_up_logging; use ::ruff::logging::set_up_logging;
use ::ruff::message::Message; use ::ruff::message::Message;
use ::ruff::printer::{Printer, SerializationFormat}; use ::ruff::printer::{Printer, SerializationFormat};
use ::ruff::settings::FilePattern; use ::ruff::pyproject;
use ::ruff::settings::Settings; use ::ruff::settings::{FilePattern, Settings};
use ::ruff::tell_user; use ::ruff::tell_user;
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME"); const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
@ -155,9 +155,20 @@ fn inner_main() -> Result<ExitCode> {
set_up_logging(cli.verbose)?; set_up_logging(cli.verbose)?;
let mut settings = Settings::from_paths(&cli.files); // Find the project root and pyproject.toml.
let mut printer = Printer::new(cli.format); 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() { if !cli.select.is_empty() {
settings.select(cli.select); settings.select(cli.select);
} }
@ -168,19 +179,20 @@ fn inner_main() -> Result<ExitCode> {
settings.exclude = cli settings.exclude = cli
.exclude .exclude
.iter() .iter()
.map(|path| FilePattern::from_user(path)) .map(|path| FilePattern::from_user(path, &project_root))
.collect(); .collect();
} }
if !cli.extend_exclude.is_empty() { if !cli.extend_exclude.is_empty() {
settings.extend_exclude = cli settings.extend_exclude = cli
.extend_exclude .extend_exclude
.iter() .iter()
.map(|path| FilePattern::from_user(path)) .map(|path| FilePattern::from_user(path, &project_root))
.collect(); .collect();
} }
cache::init()?; cache::init()?;
let mut printer = Printer::new(cli.format);
if cli.watch { if cli.watch {
if cli.fix { if cli.fix {
println!("Warning: --fix is not enabled in watch mode."); println!("Warning: --fix is not enabled in watch mode.");

View File

@ -3,31 +3,30 @@ use std::path::{Path, PathBuf};
use anyhow::Result; use anyhow::Result;
use common_path::common_path_all; use common_path::common_path_all;
use log::debug;
use path_absolutize::path_dedot; use path_absolutize::path_dedot;
use serde::Deserialize; use serde::Deserialize;
use crate::checks::CheckCode; use crate::checks::CheckCode;
use crate::fs; use crate::fs;
pub fn load_config(paths: &[PathBuf]) -> Config { pub fn load_config(pyproject: &Option<PathBuf>) -> Config {
let project_root = find_project_root(paths); match pyproject {
match find_pyproject_toml(project_root.as_deref()) { Some(pyproject) => match parse_pyproject_toml(pyproject) {
Some(path) => { Ok(pyproject) => pyproject
debug!("Found pyproject.toml at: {:?}", path); .tool
match parse_pyproject_toml(&path) { .and_then(|tool| tool.ruff)
Ok(pyproject) => pyproject .unwrap_or_default(),
.tool Err(e) => {
.and_then(|tool| tool.ruff) println!("Failed to load pyproject.toml: {:?}", e);
.unwrap_or_default(), println!("Falling back to default configuration...");
Err(e) => { Default::default()
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<PyProject> {
toml::from_str(&contents).map_err(|e| e.into()) toml::from_str(&contents).map_err(|e| e.into())
} }
fn find_pyproject_toml(path: Option<&Path>) -> Option<PathBuf> { pub fn find_pyproject_toml(path: &Option<PathBuf>) -> Option<PathBuf> {
if let Some(path) = path { if let Some(path) = path {
let path_pyproject_toml = path.join("pyproject.toml"); let path_pyproject_toml = path.join("pyproject.toml");
if path_pyproject_toml.is_file() { if path_pyproject_toml.is_file() {
@ -71,10 +70,14 @@ fn find_user_pyproject_toml() -> Option<PathBuf> {
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");
Some(path) if path.is_file() {
Some(path)
} else {
None
}
} }
fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> { pub fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
let cwd = path_dedot::CWD.deref(); let cwd = path_dedot::CWD.deref();
let absolute_sources: Vec<PathBuf> = sources.iter().map(|source| cwd.join(source)).collect(); 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)) { if let Some(prefix) = common_path_all(absolute_sources.iter().map(PathBuf::as_path)) {
@ -250,14 +253,14 @@ other-attribute = 1
#[test] #[test]
fn find_and_parse_pyproject_toml() -> Result<()> { fn find_and_parse_pyproject_toml() -> Result<()> {
let cwd = current_dir().unwrap_or_else(|_| ".".into()); let cwd = current_dir()?;
let project_root = let project_root =
find_project_root(&[PathBuf::from("resources/test/fixtures/__init__.py")]) find_project_root(&[PathBuf::from("resources/test/fixtures/__init__.py")])
.expect("Unable to find project root."); .expect("Unable to find project root.");
assert_eq!(project_root, cwd.join("resources/test/fixtures")); assert_eq!(project_root, cwd.join("resources/test/fixtures"));
let path = 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")); assert_eq!(path, cwd.join("resources/test/fixtures/pyproject.toml"));
let pyproject = parse_pyproject_toml(&path)?; let pyproject = parse_pyproject_toml(&path)?;
@ -273,7 +276,7 @@ other-attribute = 1
extend_exclude: Some(vec![ extend_exclude: Some(vec![
"excluded.py".to_string(), "excluded.py".to_string(),
"migrations".to_string(), "migrations".to_string(),
"resources/test/fixtures/directory/also_excluded.py".to_string(), "directory/also_excluded.py".to_string(),
]), ]),
select: None, select: None,
ignore: None, ignore: None,

View File

@ -16,9 +16,14 @@ pub enum FilePattern {
} }
impl FilePattern { impl FilePattern {
pub fn from_user(pattern: &str) -> Self { pub fn from_user(pattern: &str, project_root: &Option<PathBuf>) -> Self {
let absolute = Pattern::new(&fs::normalize_path(Path::new(pattern)).to_string_lossy()) let path = Path::new(pattern);
.expect("Invalid 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) { let basename = if !pattern.contains(std::path::MAIN_SEPARATOR) {
Some(Pattern::new(pattern).expect("Invalid pattern.")) Some(Pattern::new(pattern).expect("Invalid pattern."))
} else { } else {
@ -71,8 +76,8 @@ static DEFAULT_EXCLUDE: Lazy<Vec<FilePattern>> = Lazy::new(|| {
}); });
impl Settings { impl Settings {
pub fn from_paths(paths: &[PathBuf]) -> Self { pub fn from_pyproject(path: &Option<PathBuf>, project_root: &Option<PathBuf>) -> Self {
let config = load_config(paths); let config = load_config(path);
let mut settings = Settings { let mut settings = Settings {
line_length: config.line_length.unwrap_or(88), line_length: config.line_length.unwrap_or(88),
exclude: config exclude: config
@ -80,7 +85,7 @@ impl Settings {
.map(|paths| { .map(|paths| {
paths paths
.iter() .iter()
.map(|path| FilePattern::from_user(path)) .map(|path| FilePattern::from_user(path, project_root))
.collect() .collect()
}) })
.unwrap_or_else(|| DEFAULT_EXCLUDE.clone()), .unwrap_or_else(|| DEFAULT_EXCLUDE.clone()),
@ -89,7 +94,7 @@ impl Settings {
.map(|paths| { .map(|paths| {
paths paths
.iter() .iter()
.map(|path| FilePattern::from_user(path)) .map(|path| FilePattern::from_user(path, project_root))
.collect() .collect()
}) })
.unwrap_or_default(), .unwrap_or_default(),