mirror of https://github.com/astral-sh/ruff
Treat relative excludes as relative to project root (#228)
This commit is contained in:
parent
a0b50d7ebc
commit
fa0954fe47
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@ line-length = 88
|
|||
extend-exclude = [
|
||||
"excluded.py",
|
||||
"migrations",
|
||||
"resources/test/fixtures/directory/also_excluded.py",
|
||||
"directory/also_excluded.py",
|
||||
]
|
||||
|
|
|
|||
73
src/fs.rs
73
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<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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
24
src/main.rs
24
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<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.");
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
Loading…
Reference in New Issue