Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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