diff --git a/crates/ruff_cli/src/cache.rs b/crates/ruff_cli/src/cache.rs index 9b26c11809e4e..f71d26244a99e 100644 --- a/crates/ruff_cli/src/cache.rs +++ b/crates/ruff_cli/src/cache.rs @@ -25,10 +25,9 @@ use ruff_notebook::NotebookIndex; use ruff_python_ast::imports::ImportMap; use ruff_source_file::SourceFileBuilder; use ruff_text_size::{TextRange, TextSize}; -use ruff_workspace::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy, Resolver}; +use ruff_workspace::resolver::Resolver; use ruff_workspace::Settings; -use crate::cache; use crate::diagnostics::Diagnostics; /// [`Path`] that is relative to the package root in [`PackageCache`]. @@ -443,7 +442,7 @@ pub(super) struct CacheMessage { pub(crate) trait PackageCaches { fn get(&self, package_root: &Path) -> Option<&Cache>; - fn persist(self) -> anyhow::Result<()>; + fn persist(self) -> Result<()>; } impl PackageCaches for Option @@ -469,27 +468,17 @@ pub(crate) struct PackageCacheMap<'a>(FxHashMap<&'a Path, Cache>); impl<'a> PackageCacheMap<'a> { pub(crate) fn init( - pyproject_config: &PyprojectConfig, package_roots: &FxHashMap<&'a Path, Option<&'a Path>>, resolver: &Resolver, ) -> Self { fn init_cache(path: &Path) { - if let Err(e) = cache::init(path) { + if let Err(e) = init(path) { error!("Failed to initialize cache at {}: {e:?}", path.display()); } } - match pyproject_config.strategy { - PyprojectDiscoveryStrategy::Fixed => { - init_cache(&pyproject_config.settings.cache_dir); - } - PyprojectDiscoveryStrategy::Hierarchical => { - for settings in - std::iter::once(&pyproject_config.settings).chain(resolver.settings()) - { - init_cache(&settings.cache_dir); - } - } + for settings in resolver.settings() { + init_cache(&settings.cache_dir); } Self( @@ -499,7 +488,7 @@ impl<'a> PackageCacheMap<'a> { .unique() .par_bridge() .map(|cache_root| { - let settings = resolver.resolve(cache_root, pyproject_config); + let settings = resolver.resolve(cache_root); let cache = Cache::open(cache_root.to_path_buf(), settings); (cache_root, cache) }) diff --git a/crates/ruff_cli/src/commands/add_noqa.rs b/crates/ruff_cli/src/commands/add_noqa.rs index 541e40f2b78cf..4767e8c490278 100644 --- a/crates/ruff_cli/src/commands/add_noqa.rs +++ b/crates/ruff_cli/src/commands/add_noqa.rs @@ -38,7 +38,6 @@ pub(crate) fn add_noqa( .flatten() .map(ResolvedFile::path) .collect::>(), - pyproject_config, ); let start = Instant::now(); @@ -57,7 +56,7 @@ pub(crate) fn add_noqa( .parent() .and_then(|parent| package_roots.get(parent)) .and_then(|package| *package); - let settings = resolver.resolve(path, pyproject_config); + let settings = resolver.resolve(path); let source_kind = match SourceKind::from_path(path, source_type) { Ok(Some(source_kind)) => source_kind, Ok(None) => return None, diff --git a/crates/ruff_cli/src/commands/check.rs b/crates/ruff_cli/src/commands/check.rs index fb5332277ebab..71e38c5988bf0 100644 --- a/crates/ruff_cli/src/commands/check.rs +++ b/crates/ruff_cli/src/commands/check.rs @@ -57,16 +57,11 @@ pub(crate) fn check( .flatten() .map(ResolvedFile::path) .collect::>(), - pyproject_config, ); // Load the caches. let caches = if bool::from(cache) { - Some(PackageCacheMap::init( - pyproject_config, - &package_roots, - &resolver, - )) + Some(PackageCacheMap::init(&package_roots, &resolver)) } else { None }; @@ -81,7 +76,7 @@ pub(crate) fn check( .and_then(|parent| package_roots.get(parent)) .and_then(|package| *package); - let settings = resolver.resolve(path, pyproject_config); + let settings = resolver.resolve(path); if (settings.file_resolver.force_exclude || !resolved_file.is_root()) && match_exclusion( @@ -128,7 +123,7 @@ pub(crate) fn check( Some(result.unwrap_or_else(|(path, message)| { if let Some(path) = &path { - let settings = resolver.resolve(path, pyproject_config); + let settings = resolver.resolve(path); if settings.linter.rules.enabled(Rule::IOError) { let dummy = SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish(); diff --git a/crates/ruff_cli/src/commands/check_stdin.rs b/crates/ruff_cli/src/commands/check_stdin.rs index c2f1aaaafca5e..0471edd37804f 100644 --- a/crates/ruff_cli/src/commands/check_stdin.rs +++ b/crates/ruff_cli/src/commands/check_stdin.rs @@ -4,7 +4,7 @@ use anyhow::Result; use ruff_linter::packaging; use ruff_linter::settings::flags; -use ruff_workspace::resolver::{match_exclusion, python_file_at_path, PyprojectConfig}; +use ruff_workspace::resolver::{match_exclusion, python_file_at_path, PyprojectConfig, Resolver}; use crate::args::CliOverrides; use crate::diagnostics::{lint_stdin, Diagnostics}; @@ -18,20 +18,20 @@ pub(crate) fn check_stdin( noqa: flags::Noqa, fix_mode: flags::FixMode, ) -> Result { - if pyproject_config.settings.file_resolver.force_exclude { + let mut resolver = Resolver::new(pyproject_config); + + if resolver.force_exclude() { if let Some(filename) = filename { - if !python_file_at_path(filename, pyproject_config, overrides)? { + if !python_file_at_path(filename, &mut resolver, overrides)? { if fix_mode.is_apply() { parrot_stdin()?; } return Ok(Diagnostics::default()); } - let lint_settings = &pyproject_config.settings.linter; - if filename - .file_name() - .is_some_and(|name| match_exclusion(filename, name, &lint_settings.exclude)) - { + if filename.file_name().is_some_and(|name| { + match_exclusion(filename, name, &resolver.base_settings().linter.exclude) + }) { if fix_mode.is_apply() { parrot_stdin()?; } @@ -41,13 +41,13 @@ pub(crate) fn check_stdin( } let stdin = read_from_stdin()?; let package_root = filename.and_then(Path::parent).and_then(|path| { - packaging::detect_package_root(path, &pyproject_config.settings.linter.namespace_packages) + packaging::detect_package_root(path, &resolver.base_settings().linter.namespace_packages) }); let mut diagnostics = lint_stdin( filename, package_root, stdin, - &pyproject_config.settings, + resolver.base_settings(), noqa, fix_mode, )?; diff --git a/crates/ruff_cli/src/commands/format.rs b/crates/ruff_cli/src/commands/format.rs index 096cfeb43760c..c9287e459c431 100644 --- a/crates/ruff_cli/src/commands/format.rs +++ b/crates/ruff_cli/src/commands/format.rs @@ -25,9 +25,7 @@ use ruff_linter::warn_user_once; use ruff_python_ast::{PySourceType, SourceType}; use ruff_python_formatter::{format_module_source, FormatModuleError, QuoteStyle}; use ruff_text_size::{TextLen, TextRange, TextSize}; -use ruff_workspace::resolver::{ - match_exclusion, python_files_in_path, PyprojectConfig, ResolvedFile, Resolver, -}; +use ruff_workspace::resolver::{match_exclusion, python_files_in_path, ResolvedFile, Resolver}; use ruff_workspace::FormatterSettings; use crate::args::{CliOverrides, FormatArguments}; @@ -79,7 +77,7 @@ pub(crate) fn format( return Ok(ExitStatus::Success); } - warn_incompatible_formatter_settings(&pyproject_config, Some(&resolver)); + warn_incompatible_formatter_settings(&resolver); // Discover the package root for each Python file. let package_roots = resolver.package_roots( @@ -88,7 +86,6 @@ pub(crate) fn format( .flatten() .map(ResolvedFile::path) .collect::>(), - &pyproject_config, ); let caches = if cli.no_cache { @@ -99,11 +96,7 @@ pub(crate) fn format( #[cfg(debug_assertions)] crate::warn_user!("Detected debug build without --no-cache."); - Some(PackageCacheMap::init( - &pyproject_config, - &package_roots, - &resolver, - )) + Some(PackageCacheMap::init(&package_roots, &resolver)) }; let start = Instant::now(); @@ -118,7 +111,7 @@ pub(crate) fn format( return None; }; - let settings = resolver.resolve(path, &pyproject_config); + let settings = resolver.resolve(path); // Ignore files that are excluded from formatting if (settings.file_resolver.force_exclude || !resolved_file.is_root()) @@ -723,15 +716,10 @@ impl Display for FormatCommandError { } } -pub(super) fn warn_incompatible_formatter_settings( - pyproject_config: &PyprojectConfig, - resolver: Option<&Resolver>, -) { +pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) { // First, collect all rules that are incompatible regardless of the linter-specific settings. let mut incompatible_rules = FxHashSet::default(); - for setting in std::iter::once(&pyproject_config.settings) - .chain(resolver.iter().flat_map(|resolver| resolver.settings())) - { + for setting in resolver.settings() { for rule in [ // The formatter might collapse implicit string concatenation on a single line. Rule::SingleLineImplicitStringConcatenation, @@ -760,9 +748,7 @@ pub(super) fn warn_incompatible_formatter_settings( } // Next, validate settings-specific incompatibilities. - for setting in std::iter::once(&pyproject_config.settings) - .chain(resolver.iter().flat_map(|resolver| resolver.settings())) - { + for setting in resolver.settings() { // Validate all rules that rely on tab styles. if setting.linter.rules.enabled(Rule::TabIndentation) && setting.formatter.indent_style.is_tab() diff --git a/crates/ruff_cli/src/commands/format_stdin.rs b/crates/ruff_cli/src/commands/format_stdin.rs index 01c6e29b141a4..33fa5160424fe 100644 --- a/crates/ruff_cli/src/commands/format_stdin.rs +++ b/crates/ruff_cli/src/commands/format_stdin.rs @@ -6,7 +6,7 @@ use log::error; use ruff_linter::source_kind::SourceKind; use ruff_python_ast::{PySourceType, SourceType}; -use ruff_workspace::resolver::{match_exclusion, python_file_at_path}; +use ruff_workspace::resolver::{match_exclusion, python_file_at_path, Resolver}; use ruff_workspace::FormatterSettings; use crate::args::{CliOverrides, FormatArguments}; @@ -27,24 +27,23 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R cli.stdin_filename.as_deref(), )?; - warn_incompatible_formatter_settings(&pyproject_config, None); + let mut resolver = Resolver::new(&pyproject_config); + warn_incompatible_formatter_settings(&resolver); let mode = FormatMode::from_cli(cli); - if pyproject_config.settings.file_resolver.force_exclude { + if resolver.force_exclude() { if let Some(filename) = cli.stdin_filename.as_deref() { - if !python_file_at_path(filename, &pyproject_config, overrides)? { + if !python_file_at_path(filename, &mut resolver, overrides)? { if mode.is_write() { parrot_stdin()?; } return Ok(ExitStatus::Success); } - let format_settings = &pyproject_config.settings.formatter; - if filename - .file_name() - .is_some_and(|name| match_exclusion(filename, name, &format_settings.exclude)) - { + if filename.file_name().is_some_and(|name| { + match_exclusion(filename, name, &resolver.base_settings().formatter.exclude) + }) { if mode.is_write() { parrot_stdin()?; } @@ -63,12 +62,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R }; // Format the file. - match format_source_code( - path, - &pyproject_config.settings.formatter, - source_type, - mode, - ) { + match format_source_code(path, &resolver.base_settings().formatter, source_type, mode) { Ok(result) => match mode { FormatMode::Write => Ok(ExitStatus::Success), FormatMode::Check | FormatMode::Diff => { diff --git a/crates/ruff_cli/src/commands/show_settings.rs b/crates/ruff_cli/src/commands/show_settings.rs index baeed55a08f36..2e27fb344ada3 100644 --- a/crates/ruff_cli/src/commands/show_settings.rs +++ b/crates/ruff_cli/src/commands/show_settings.rs @@ -29,7 +29,7 @@ pub(crate) fn show_settings( bail!("No files found under the given path"); }; - let settings = resolver.resolve(&path, pyproject_config); + let settings = resolver.resolve(&path); writeln!(writer, "Resolved settings for: {path:?}")?; if let Some(settings_path) = pyproject_config.path.as_ref() { diff --git a/crates/ruff_dev/src/format_dev.rs b/crates/ruff_dev/src/format_dev.rs index 2de483ffe00ea..be52ec863e467 100644 --- a/crates/ruff_dev/src/format_dev.rs +++ b/crates/ruff_dev/src/format_dev.rs @@ -27,7 +27,7 @@ use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::EnvFilter; -use ruff_cli::args::{FormatCommand, LogLevelArgs}; +use ruff_cli::args::{CliOverrides, FormatArguments, FormatCommand, LogLevelArgs}; use ruff_cli::resolve::resolve; use ruff_formatter::{FormatError, LineWidth, PrintError}; use ruff_linter::logging::LogLevel; @@ -38,24 +38,24 @@ use ruff_python_formatter::{ use ruff_python_parser::ParseError; use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile, Resolver}; -/// Find files that ruff would check so we can format them. Adapted from `ruff_cli`. -#[allow(clippy::type_complexity)] -fn ruff_check_paths( - dirs: &[PathBuf], -) -> anyhow::Result<( - Vec>, - Resolver, - PyprojectConfig, -)> { +fn parse_cli(dirs: &[PathBuf]) -> anyhow::Result<(FormatArguments, CliOverrides)> { let args_matches = FormatCommand::command() .no_binary_name(true) .get_matches_from(dirs); let arguments: FormatCommand = FormatCommand::from_arg_matches(&args_matches)?; let (cli, overrides) = arguments.partition(); + Ok((cli, overrides)) +} + +/// Find the [`PyprojectConfig`] to use for formatting. +fn find_pyproject_config( + cli: &FormatArguments, + overrides: &CliOverrides, +) -> anyhow::Result { let mut pyproject_config = resolve( cli.isolated, cli.config.as_deref(), - &overrides, + overrides, cli.stdin_filename.as_deref(), )?; // We don't want to format pyproject.toml @@ -64,11 +64,18 @@ fn ruff_check_paths( FilePattern::Builtin("*.pyi"), ]) .unwrap(); - let (paths, resolver) = python_files_in_path(&cli.files, &pyproject_config, &overrides)?; - if paths.is_empty() { - bail!("no python files in {:?}", dirs) - } - Ok((paths, resolver, pyproject_config)) + Ok(pyproject_config) +} + +/// Find files that ruff would check so we can format them. Adapted from `ruff_cli`. +#[allow(clippy::type_complexity)] +fn ruff_check_paths<'a>( + pyproject_config: &'a PyprojectConfig, + cli: &FormatArguments, + overrides: &CliOverrides, +) -> anyhow::Result<(Vec>, Resolver<'a>)> { + let (paths, resolver) = python_files_in_path(&cli.files, pyproject_config, overrides)?; + Ok((paths, resolver)) } /// Collects statistics over the formatted files to compute the Jaccard index or the similarity @@ -452,11 +459,17 @@ fn format_dev_project( files[0].display() ); - // TODO(konstin): black excludes + // TODO(konstin): Respect black's excludes. // Find files to check (or in this case, format twice). Adapted from ruff_cli // First argument is ignored - let (paths, resolver, pyproject_config) = ruff_check_paths(files)?; + let (cli, overrides) = parse_cli(files)?; + let pyproject_config = find_pyproject_config(&cli, &overrides)?; + let (paths, resolver) = ruff_check_paths(&pyproject_config, &cli, &overrides)?; + + if paths.is_empty() { + bail!("No Python files found under the given path(s)"); + } let results = { let pb_span = @@ -469,14 +482,7 @@ fn format_dev_project( #[cfg(feature = "singlethreaded")] let iter = { paths.into_iter() }; iter.map(|path| { - let result = format_dir_entry( - path, - stability_check, - write, - &black_options, - &resolver, - &pyproject_config, - ); + let result = format_dir_entry(path, stability_check, write, &black_options, &resolver); pb_span.pb_inc(1); result }) @@ -526,14 +532,13 @@ fn format_dev_project( }) } -/// Error handling in between walkdir and `format_dev_file` +/// Error handling in between walkdir and `format_dev_file`. fn format_dir_entry( resolved_file: Result, stability_check: bool, write: bool, options: &BlackOptions, resolver: &Resolver, - pyproject_config: &PyprojectConfig, ) -> anyhow::Result<(Result, PathBuf), Error> { let resolved_file = resolved_file.context("Iterating the files in the repository failed")?; // For some reason it does not filter in the beginning @@ -544,7 +549,7 @@ fn format_dir_entry( let path = resolved_file.into_path(); let mut options = options.to_py_format_options(&path); - let settings = resolver.resolve(&path, pyproject_config); + let settings = resolver.resolve(&path); // That's a bad way of doing this but it's not worth doing something better for format_dev if settings.formatter.line_width != LineWidth::default() { options = options.with_line_width(settings.formatter.line_width); diff --git a/crates/ruff_workspace/src/resolver.rs b/crates/ruff_workspace/src/resolver.rs index 2c6108f828974..590eb3e2d7f83 100644 --- a/crates/ruff_workspace/src/resolver.rs +++ b/crates/ruff_workspace/src/resolver.rs @@ -11,7 +11,7 @@ use anyhow::Result; use anyhow::{anyhow, bail}; use globset::{Candidate, GlobSet}; use ignore::{WalkBuilder, WalkState}; -use itertools::Itertools; +use itertools::{Either, Itertools}; use log::debug; use path_absolutize::path_dedot; use rustc_hash::{FxHashMap, FxHashSet}; @@ -25,6 +25,7 @@ use crate::pyproject::settings_toml; use crate::settings::Settings; /// The configuration information from a `pyproject.toml` file. +#[derive(Debug)] pub struct PyprojectConfig { /// The strategy used to discover the relevant `pyproject.toml` file for /// each Python file. @@ -63,10 +64,12 @@ pub enum PyprojectDiscoveryStrategy { } impl PyprojectDiscoveryStrategy { + #[inline] pub const fn is_fixed(self) -> bool { matches!(self, PyprojectDiscoveryStrategy::Fixed) } + #[inline] pub const fn is_hierarchical(self) -> bool { matches!(self, PyprojectDiscoveryStrategy::Hierarchical) } @@ -94,40 +97,68 @@ impl Relativity { } } -#[derive(Default)] -pub struct Resolver { +#[derive(Debug)] +pub struct Resolver<'a> { + pyproject_config: &'a PyprojectConfig, settings: BTreeMap, } -impl Resolver { +impl<'a> Resolver<'a> { + /// Create a new [`Resolver`] for the given [`PyprojectConfig`]. + pub fn new(pyproject_config: &'a PyprojectConfig) -> Self { + Self { + pyproject_config, + settings: BTreeMap::new(), + } + } + + /// Return the [`Settings`] from the [`PyprojectConfig`]. + #[inline] + pub fn base_settings(&self) -> &Settings { + &self.pyproject_config.settings + } + + /// Return `true` if the [`Resolver`] is using a hierarchical discovery strategy. + #[inline] + pub fn is_hierarchical(&self) -> bool { + self.pyproject_config.strategy.is_hierarchical() + } + + /// Return `true` if the [`Resolver`] should force-exclude files passed directly to the CLI. + #[inline] + pub fn force_exclude(&self) -> bool { + self.pyproject_config.settings.file_resolver.force_exclude + } + + /// Return `true` if the [`Resolver`] should respect `.gitignore` files. + #[inline] + pub fn respect_gitignore(&self) -> bool { + self.pyproject_config + .settings + .file_resolver + .respect_gitignore + } + /// Add a resolved [`Settings`] under a given [`PathBuf`] scope. fn add(&mut self, path: PathBuf, settings: Settings) { self.settings.insert(path, settings); } /// Return the appropriate [`Settings`] for a given [`Path`]. - pub fn resolve<'a>( - &'a self, - path: &Path, - pyproject_config: &'a PyprojectConfig, - ) -> &'a Settings { - match pyproject_config.strategy { - PyprojectDiscoveryStrategy::Fixed => &pyproject_config.settings, + pub fn resolve(&self, path: &Path) -> &Settings { + match self.pyproject_config.strategy { + PyprojectDiscoveryStrategy::Fixed => &self.pyproject_config.settings, PyprojectDiscoveryStrategy::Hierarchical => self .settings .iter() .rev() .find_map(|(root, settings)| path.starts_with(root).then_some(settings)) - .unwrap_or(&pyproject_config.settings), + .unwrap_or(&self.pyproject_config.settings), } } /// Return a mapping from Python package to its package root. - pub fn package_roots<'a>( - &'a self, - files: &[&'a Path], - pyproject_config: &'a PyprojectConfig, - ) -> FxHashMap<&'a Path, Option<&'a Path>> { + pub fn package_roots(&'a self, files: &[&'a Path]) -> FxHashMap<&'a Path, Option<&'a Path>> { // Pre-populate the module cache, since the list of files could (but isn't // required to) contain some `__init__.py` files. let mut package_cache: FxHashMap<&Path, bool> = FxHashMap::default(); @@ -154,10 +185,7 @@ impl Resolver { std::collections::hash_map::Entry::Occupied(_) => continue, std::collections::hash_map::Entry::Vacant(entry) => { let namespace_packages = if has_namespace_packages { - self.resolve(file, pyproject_config) - .linter - .namespace_packages - .as_slice() + self.resolve(file).linter.namespace_packages.as_slice() } else { &[] }; @@ -176,7 +204,12 @@ impl Resolver { /// Return an iterator over the resolved [`Settings`] in this [`Resolver`]. pub fn settings(&self) -> impl Iterator { - self.settings.values() + match self.pyproject_config.strategy { + PyprojectDiscoveryStrategy::Fixed => { + Either::Left(std::iter::once(&self.pyproject_config.settings)) + } + PyprojectDiscoveryStrategy::Hierarchical => Either::Right(self.settings.values()), + } } } @@ -288,18 +321,18 @@ pub fn resolve_root_settings( } /// Find all Python (`.py`, `.pyi` and `.ipynb` files) in a set of paths. -pub fn python_files_in_path( +pub fn python_files_in_path<'a>( paths: &[PathBuf], - pyproject_config: &PyprojectConfig, + pyproject_config: &'a PyprojectConfig, transformer: &dyn ConfigurationTransformer, -) -> Result<(Vec>, Resolver)> { +) -> Result<(Vec>, Resolver<'a>)> { // Normalize every path (e.g., convert from relative to absolute). let mut paths: Vec = paths.iter().map(fs::normalize_path).unique().collect(); // Search for `pyproject.toml` files in all parent directories. - let mut resolver = Resolver::default(); + let mut resolver = Resolver::new(pyproject_config); let mut seen = FxHashSet::default(); - if pyproject_config.strategy.is_hierarchical() { + if resolver.is_hierarchical() { for path in &paths { for ancestor in path.ancestors() { if seen.insert(ancestor) { @@ -315,8 +348,8 @@ pub fn python_files_in_path( } // Check if the paths themselves are excluded. - if pyproject_config.settings.file_resolver.force_exclude { - paths.retain(|path| !is_file_excluded(path, &resolver, pyproject_config)); + if resolver.force_exclude() { + paths.retain(|path| !is_file_excluded(path, &resolver)); if paths.is_empty() { return Ok((vec![], resolver)); } @@ -330,11 +363,12 @@ pub fn python_files_in_path( for path in rest_paths { builder.add(path); } - builder.standard_filters(pyproject_config.settings.file_resolver.respect_gitignore); + builder.standard_filters(resolver.respect_gitignore()); builder.hidden(false); let walker = builder.build_parallel(); // Run the `WalkParallel` to collect all Python files. + let is_hierarchical = resolver.is_hierarchical(); let error: std::sync::Mutex> = std::sync::Mutex::new(Ok(())); let resolver: RwLock = RwLock::new(resolver); let files: std::sync::Mutex>> = @@ -346,7 +380,7 @@ pub fn python_files_in_path( if entry.depth() > 0 { let path = entry.path(); let resolver = resolver.read().unwrap(); - let settings = resolver.resolve(path, pyproject_config); + let settings = resolver.resolve(path); if let Some(file_name) = path.file_name() { let file_path = Candidate::new(path); let file_basename = Candidate::new(file_name); @@ -374,7 +408,7 @@ pub fn python_files_in_path( // Search for the `pyproject.toml` file in this directory, before we visit any // of its contents. - if pyproject_config.strategy.is_hierarchical() { + if is_hierarchical { if let Ok(entry) = &result { if entry .file_type() @@ -416,7 +450,7 @@ pub fn python_files_in_path( // Otherwise, check if the file is included. let path = entry.path(); let resolver = resolver.read().unwrap(); - let settings = resolver.resolve(path, pyproject_config); + let settings = resolver.resolve(path); if settings.file_resolver.include.is_match(path) { debug!("Included path via `include`: {:?}", path); Some(ResolvedFile::Nested(entry.into_path())) @@ -494,15 +528,14 @@ impl Ord for ResolvedFile { /// Return `true` if the Python file at [`Path`] is _not_ excluded. pub fn python_file_at_path( path: &Path, - pyproject_config: &PyprojectConfig, + resolver: &mut Resolver, transformer: &dyn ConfigurationTransformer, ) -> Result { // Normalize the path (e.g., convert from relative to absolute). let path = fs::normalize_path(path); // Search for `pyproject.toml` files in all parent directories. - let mut resolver = Resolver::default(); - if pyproject_config.strategy.is_hierarchical() { + if resolver.is_hierarchical() { for ancestor in path.ancestors() { if let Some(pyproject) = settings_toml(ancestor)? { let (root, settings) = @@ -514,18 +547,14 @@ pub fn python_file_at_path( } // Check exclusions. - Ok(!is_file_excluded(&path, &resolver, pyproject_config)) + Ok(!is_file_excluded(&path, resolver)) } /// Return `true` if the given top-level [`Path`] should be excluded. -fn is_file_excluded( - path: &Path, - resolver: &Resolver, - pyproject_strategy: &PyprojectConfig, -) -> bool { +fn is_file_excluded(path: &Path, resolver: &Resolver) -> bool { // TODO(charlie): Respect gitignore. for path in path.ancestors() { - let settings = resolver.resolve(path, pyproject_strategy); + let settings = resolver.resolve(path); if let Some(file_name) = path.file_name() { let file_path = Candidate::new(path); let file_basename = Candidate::new(file_name); @@ -618,7 +647,6 @@ mod tests { #[test] fn rooted_exclusion() -> Result<()> { let package_root = test_resource_path("package"); - let resolver = Resolver::default(); let pyproject_config = PyprojectConfig::new( PyprojectDiscoveryStrategy::Hierarchical, resolve_root_settings( @@ -628,20 +656,19 @@ mod tests { )?, None, ); + let resolver = Resolver::new(&pyproject_config); // src/app.py should not be excluded even if it lives in a hierarchy that should // be excluded by virtue of the pyproject.toml having `resources/*` in // it. assert!(!is_file_excluded( &package_root.join("src/app.py"), &resolver, - &pyproject_config, )); // However, resources/ignored.py should be ignored, since that `resources` is // beneath the package root. assert!(is_file_excluded( &package_root.join("resources/ignored.py"), &resolver, - &pyproject_config, )); Ok(()) }