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` ).
- 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

View File

@ -3,5 +3,5 @@ line-length = 88
extend-exclude = [
"excluded.py",
"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 {
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<str> {
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<str> {
path.to_string_lossy()
}
/// Read a file's contents from disk.
pub fn read_file(path: &Path) -> Result<String> {
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));

View File

@ -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;

View File

@ -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<ExitCode> {
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<ExitCode> {
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.");

View File

@ -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<PathBuf>) -> 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<PyProject> {
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 {
let path_pyproject_toml = path.join("pyproject.toml");
if path_pyproject_toml.is_file() {
@ -71,10 +70,14 @@ fn find_user_pyproject_toml() -> Option<PathBuf> {
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<PathBuf> {
pub 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)) {
@ -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,

View File

@ -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<PathBuf>) -> 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<Vec<FilePattern>> = Lazy::new(|| {
});
impl Settings {
pub fn from_paths(paths: &[PathBuf]) -> Self {
let config = load_config(paths);
pub fn from_pyproject(path: &Option<PathBuf>, project_root: &Option<PathBuf>) -> 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(),