Skip to content

Commit

Permalink
Support hierarchical settings for nested directories
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Dec 11, 2022
1 parent 2417965 commit 0de5bcb
Show file tree
Hide file tree
Showing 18 changed files with 323 additions and 161 deletions.
22 changes: 17 additions & 5 deletions src/commands.rs
Original file line number Diff line number Diff line change
@@ -1,33 +1,45 @@
use std::path::PathBuf;
use std::path::{Path, PathBuf};

use anyhow::{bail, Result};
use serde::Serialize;
use walkdir::DirEntry;

use crate::checks::CheckCode;
use crate::cli::Overrides;
use crate::fs::iter_python_files;
use crate::resolver::{discover_settings, Resolver};
use crate::settings::types::SerializationFormat;
use crate::{Configuration, Settings};

/// Print the user-facing configuration settings.
pub fn show_settings(
configuration: &Configuration,
project_root: Option<&PathBuf>,
pyproject: Option<&PathBuf>,
project_root: Option<&Path>,
pyproject: Option<&Path>,
) {
println!("Resolved configuration: {configuration:#?}");
println!("Found project root at: {project_root:?}");
println!("Found pyproject.toml at: {pyproject:?}");
}

/// Show the list of files to be checked based on current settings.
pub fn show_files(files: &[PathBuf], settings: &Settings) {
pub fn show_files(files: &[PathBuf], default: &Settings, overrides: &Overrides) {
// Discover the settings for the filesystem hierarchy.
let settings = discover_settings(files, overrides);
let resolver = Resolver {
default,
settings: &settings,
};

// Collect all files in the hierarchy.
let mut entries: Vec<DirEntry> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
.flat_map(|path| iter_python_files(path, &resolver))
.flatten()
.collect();
entries.sort_by(|a, b| a.path().cmp(b.path()));

// Print the list of files.
for entry in entries {
println!("{}", entry.path().to_string_lossy());
}
Expand Down
76 changes: 53 additions & 23 deletions src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use rustc_hash::FxHashSet;
use walkdir::{DirEntry, WalkDir};

use crate::checks::CheckCode;
use crate::resolver::Resolver;

/// Extract the absolute path and basename (as strings) from a Path.
fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
Expand All @@ -30,33 +31,62 @@ fn is_excluded(file_path: &str, file_basename: &str, exclude: &globset::GlobSet)
}

fn is_included(path: &Path) -> bool {
let file_name = path.to_string_lossy();
file_name.ends_with(".py") || file_name.ends_with(".pyi")
path.extension()
.map_or(false, |ext| ext == "py" || ext == "pyi")
}

/// Find all `pyproject.toml` files for a given `Path`. Both parents and
/// children will be included in the resulting `Vec`.
pub fn iter_pyproject_files(path: &Path) -> Vec<PathBuf> {
let mut paths = Vec::new();

// Search for `pyproject.toml` files in all parent directories.
let path = normalize_path(path);
for path in path.ancestors() {
if path.is_dir() {
let toml_path = path.join("pyproject.toml");
if toml_path.exists() {
paths.push(toml_path);
}
}
}

// Search for `pyproject.toml` files in all child directories.
for path in WalkDir::new(path)
.into_iter()
.filter_entry(|entry| {
entry.file_name().to_str().map_or(false, |file_name| {
entry.depth() == 0 || !file_name.starts_with('.')
})
})
.filter_map(std::result::Result::ok)
.filter(|entry| entry.path().ends_with("pyproject.toml"))
{
paths.push(path.into_path());
}

paths
}

/// Find all Python (`.py` and `.pyi` files) in a given `Path`.
pub fn iter_python_files<'a>(
path: &'a Path,
exclude: &'a globset::GlobSet,
extend_exclude: &'a globset::GlobSet,
resolver: &'a Resolver<'a>,
) -> impl Iterator<Item = Result<DirEntry, walkdir::Error>> + 'a {
// Run some checks over the provided patterns, to enable optimizations below.
let has_exclude = !exclude.is_empty();
let has_extend_exclude = !extend_exclude.is_empty();

WalkDir::new(normalize_path(path))
.into_iter()
.filter_entry(move |entry| {
if !has_exclude && !has_extend_exclude {
return true;
}

let path = entry.path();
let settings = resolver.resolve(path);
let exclude = &settings.exclude;
let extend_exclude = &settings.extend_exclude;

match extract_path_names(path) {
Ok((file_path, file_basename)) => {
if has_exclude && is_excluded(file_path, file_basename, exclude) {
if !exclude.is_empty() && is_excluded(file_path, file_basename, exclude) {
debug!("Ignored path via `exclude`: {:?}", path);
false
} else if has_extend_exclude
} else if !extend_exclude.is_empty()
&& is_excluded(file_path, file_basename, extend_exclude)
{
debug!("Ignored path via `extend-exclude`: {:?}", path);
Expand Down Expand Up @@ -131,7 +161,7 @@ pub(crate) fn read_file(path: &Path) -> Result<String> {

#[cfg(test)]
mod tests {
use std::path::{Path, PathBuf};
use std::path::Path;

use anyhow::Result;
use globset::GlobSet;
Expand All @@ -155,7 +185,7 @@ mod tests {
assert!(!is_included(&path));
}

fn make_exclusion(file_pattern: FilePattern, project_root: Option<&PathBuf>) -> GlobSet {
fn make_exclusion(file_pattern: FilePattern, project_root: Option<&Path>) -> GlobSet {
let mut builder = globset::GlobSetBuilder::new();
file_pattern.add_to(&mut builder, project_root).unwrap();
builder.build().unwrap()
Expand All @@ -171,7 +201,7 @@ mod tests {
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
&make_exclusion(exclude, Some(project_root))
));

let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
Expand All @@ -180,7 +210,7 @@ mod tests {
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
&make_exclusion(exclude, Some(project_root))
));

let path = Path::new("foo/bar/baz.py")
Expand All @@ -191,7 +221,7 @@ mod tests {
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
&make_exclusion(exclude, Some(project_root))
));

let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
Expand All @@ -200,7 +230,7 @@ mod tests {
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
&make_exclusion(exclude, Some(project_root))
));

let path = Path::new("foo/bar/baz.py")
Expand All @@ -211,7 +241,7 @@ mod tests {
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
&make_exclusion(exclude, Some(project_root))
));

let path = Path::new("foo/bar/baz.py")
Expand All @@ -222,7 +252,7 @@ mod tests {
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
&make_exclusion(exclude, Some(project_root))
));

let path = Path::new("foo/bar/baz.py")
Expand All @@ -233,7 +263,7 @@ mod tests {
assert!(!is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
&make_exclusion(exclude, Some(project_root))
));

Ok(())
Expand Down
16 changes: 16 additions & 0 deletions src/iterators.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;

/// Shim that calls `par_iter` except for wasm because there's no wasm support
/// in rayon yet (there is a shim to be used for the web, but it requires js
/// cooperation) Unfortunately, `ParallelIterator` does not implement `Iterator`
/// so the signatures diverge
#[cfg(not(target_family = "wasm"))]
pub fn par_iter<T: Sync>(iterable: &[T]) -> impl ParallelIterator<Item = &T> {
iterable.par_iter()
}

#[cfg(target_family = "wasm")]
pub fn par_iter<T: Sync>(iterable: &[T]) -> impl Iterator<Item = &T> {
iterable.iter()
}
6 changes: 4 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ pub mod flake8_tidy_imports;
mod flake8_unused_arguments;
pub mod fs;
mod isort;
pub mod iterators;
mod lex;
pub mod linter;
pub mod logging;
Expand All @@ -73,6 +74,7 @@ mod pygrep_hooks;
mod pylint;
mod python;
mod pyupgrade;
pub mod resolver;
mod ruff;
mod rustpython_helpers;
pub mod settings;
Expand All @@ -97,8 +99,8 @@ pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Check>> {
};

let settings = Settings::from_configuration(
Configuration::from_pyproject(pyproject.as_ref(), project_root.as_ref())?,
project_root.as_ref(),
Configuration::from_pyproject(pyproject.as_ref())?,
project_root.as_deref(),
)?;

// Tokenize once.
Expand Down
Loading

0 comments on commit 0de5bcb

Please sign in to comment.