Move pyproject_config into Resolver (#9453)

## Summary

Sort of a random PR to make the coupling between `pyproject_config` and
`resolver` more explicit by passing it to the `Resolver`, rather than
threading it through to each individual method.
This commit is contained in:
Charlie Marsh 2024-01-10 17:58:53 -05:00 committed by GitHub
parent 79f4abbb8d
commit 4a3bb67b5f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 144 additions and 149 deletions

View file

@ -25,10 +25,9 @@ use ruff_notebook::NotebookIndex;
use ruff_python_ast::imports::ImportMap; use ruff_python_ast::imports::ImportMap;
use ruff_source_file::SourceFileBuilder; use ruff_source_file::SourceFileBuilder;
use ruff_text_size::{TextRange, TextSize}; use ruff_text_size::{TextRange, TextSize};
use ruff_workspace::resolver::{PyprojectConfig, PyprojectDiscoveryStrategy, Resolver}; use ruff_workspace::resolver::Resolver;
use ruff_workspace::Settings; use ruff_workspace::Settings;
use crate::cache;
use crate::diagnostics::Diagnostics; use crate::diagnostics::Diagnostics;
/// [`Path`] that is relative to the package root in [`PackageCache`]. /// [`Path`] that is relative to the package root in [`PackageCache`].
@ -443,7 +442,7 @@ pub(super) struct CacheMessage {
pub(crate) trait PackageCaches { pub(crate) trait PackageCaches {
fn get(&self, package_root: &Path) -> Option<&Cache>; fn get(&self, package_root: &Path) -> Option<&Cache>;
fn persist(self) -> anyhow::Result<()>; fn persist(self) -> Result<()>;
} }
impl<T> PackageCaches for Option<T> impl<T> PackageCaches for Option<T>
@ -469,28 +468,18 @@ pub(crate) struct PackageCacheMap<'a>(FxHashMap<&'a Path, Cache>);
impl<'a> PackageCacheMap<'a> { impl<'a> PackageCacheMap<'a> {
pub(crate) fn init( pub(crate) fn init(
pyproject_config: &PyprojectConfig,
package_roots: &FxHashMap<&'a Path, Option<&'a Path>>, package_roots: &FxHashMap<&'a Path, Option<&'a Path>>,
resolver: &Resolver, resolver: &Resolver,
) -> Self { ) -> Self {
fn init_cache(path: &Path) { 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()); error!("Failed to initialize cache at {}: {e:?}", path.display());
} }
} }
match pyproject_config.strategy { for settings in resolver.settings() {
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); init_cache(&settings.cache_dir);
} }
}
}
Self( Self(
package_roots package_roots
@ -499,7 +488,7 @@ impl<'a> PackageCacheMap<'a> {
.unique() .unique()
.par_bridge() .par_bridge()
.map(|cache_root| { .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); let cache = Cache::open(cache_root.to_path_buf(), settings);
(cache_root, cache) (cache_root, cache)
}) })

View file

@ -38,7 +38,6 @@ pub(crate) fn add_noqa(
.flatten() .flatten()
.map(ResolvedFile::path) .map(ResolvedFile::path)
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
pyproject_config,
); );
let start = Instant::now(); let start = Instant::now();
@ -57,7 +56,7 @@ pub(crate) fn add_noqa(
.parent() .parent()
.and_then(|parent| package_roots.get(parent)) .and_then(|parent| package_roots.get(parent))
.and_then(|package| *package); .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) { let source_kind = match SourceKind::from_path(path, source_type) {
Ok(Some(source_kind)) => source_kind, Ok(Some(source_kind)) => source_kind,
Ok(None) => return None, Ok(None) => return None,

View file

@ -57,16 +57,11 @@ pub(crate) fn check(
.flatten() .flatten()
.map(ResolvedFile::path) .map(ResolvedFile::path)
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
pyproject_config,
); );
// Load the caches. // Load the caches.
let caches = if bool::from(cache) { let caches = if bool::from(cache) {
Some(PackageCacheMap::init( Some(PackageCacheMap::init(&package_roots, &resolver))
pyproject_config,
&package_roots,
&resolver,
))
} else { } else {
None None
}; };
@ -81,7 +76,7 @@ pub(crate) fn check(
.and_then(|parent| package_roots.get(parent)) .and_then(|parent| package_roots.get(parent))
.and_then(|package| *package); .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()) if (settings.file_resolver.force_exclude || !resolved_file.is_root())
&& match_exclusion( && match_exclusion(
@ -128,7 +123,7 @@ pub(crate) fn check(
Some(result.unwrap_or_else(|(path, message)| { Some(result.unwrap_or_else(|(path, message)| {
if let Some(path) = &path { if let Some(path) = &path {
let settings = resolver.resolve(path, pyproject_config); let settings = resolver.resolve(path);
if settings.linter.rules.enabled(Rule::IOError) { if settings.linter.rules.enabled(Rule::IOError) {
let dummy = let dummy =
SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish(); SourceFileBuilder::new(path.to_string_lossy().as_ref(), "").finish();

View file

@ -4,7 +4,7 @@ use anyhow::Result;
use ruff_linter::packaging; use ruff_linter::packaging;
use ruff_linter::settings::flags; 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::args::CliOverrides;
use crate::diagnostics::{lint_stdin, Diagnostics}; use crate::diagnostics::{lint_stdin, Diagnostics};
@ -18,20 +18,20 @@ pub(crate) fn check_stdin(
noqa: flags::Noqa, noqa: flags::Noqa,
fix_mode: flags::FixMode, fix_mode: flags::FixMode,
) -> Result<Diagnostics> { ) -> Result<Diagnostics> {
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 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() { if fix_mode.is_apply() {
parrot_stdin()?; parrot_stdin()?;
} }
return Ok(Diagnostics::default()); return Ok(Diagnostics::default());
} }
let lint_settings = &pyproject_config.settings.linter; if filename.file_name().is_some_and(|name| {
if filename match_exclusion(filename, name, &resolver.base_settings().linter.exclude)
.file_name() }) {
.is_some_and(|name| match_exclusion(filename, name, &lint_settings.exclude))
{
if fix_mode.is_apply() { if fix_mode.is_apply() {
parrot_stdin()?; parrot_stdin()?;
} }
@ -41,13 +41,13 @@ pub(crate) fn check_stdin(
} }
let stdin = read_from_stdin()?; let stdin = read_from_stdin()?;
let package_root = filename.and_then(Path::parent).and_then(|path| { 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( let mut diagnostics = lint_stdin(
filename, filename,
package_root, package_root,
stdin, stdin,
&pyproject_config.settings, resolver.base_settings(),
noqa, noqa,
fix_mode, fix_mode,
)?; )?;

View file

@ -25,9 +25,7 @@ use ruff_linter::warn_user_once;
use ruff_python_ast::{PySourceType, SourceType}; use ruff_python_ast::{PySourceType, SourceType};
use ruff_python_formatter::{format_module_source, FormatModuleError, QuoteStyle}; use ruff_python_formatter::{format_module_source, FormatModuleError, QuoteStyle};
use ruff_text_size::{TextLen, TextRange, TextSize}; use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_workspace::resolver::{ use ruff_workspace::resolver::{match_exclusion, python_files_in_path, ResolvedFile, Resolver};
match_exclusion, python_files_in_path, PyprojectConfig, ResolvedFile, Resolver,
};
use ruff_workspace::FormatterSettings; use ruff_workspace::FormatterSettings;
use crate::args::{CliOverrides, FormatArguments}; use crate::args::{CliOverrides, FormatArguments};
@ -79,7 +77,7 @@ pub(crate) fn format(
return Ok(ExitStatus::Success); 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. // Discover the package root for each Python file.
let package_roots = resolver.package_roots( let package_roots = resolver.package_roots(
@ -88,7 +86,6 @@ pub(crate) fn format(
.flatten() .flatten()
.map(ResolvedFile::path) .map(ResolvedFile::path)
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
&pyproject_config,
); );
let caches = if cli.no_cache { let caches = if cli.no_cache {
@ -99,11 +96,7 @@ pub(crate) fn format(
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
crate::warn_user!("Detected debug build without --no-cache."); crate::warn_user!("Detected debug build without --no-cache.");
Some(PackageCacheMap::init( Some(PackageCacheMap::init(&package_roots, &resolver))
&pyproject_config,
&package_roots,
&resolver,
))
}; };
let start = Instant::now(); let start = Instant::now();
@ -118,7 +111,7 @@ pub(crate) fn format(
return None; return None;
}; };
let settings = resolver.resolve(path, &pyproject_config); let settings = resolver.resolve(path);
// Ignore files that are excluded from formatting // Ignore files that are excluded from formatting
if (settings.file_resolver.force_exclude || !resolved_file.is_root()) 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( pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) {
pyproject_config: &PyprojectConfig,
resolver: Option<&Resolver>,
) {
// First, collect all rules that are incompatible regardless of the linter-specific settings. // First, collect all rules that are incompatible regardless of the linter-specific settings.
let mut incompatible_rules = FxHashSet::default(); let mut incompatible_rules = FxHashSet::default();
for setting in std::iter::once(&pyproject_config.settings) for setting in resolver.settings() {
.chain(resolver.iter().flat_map(|resolver| resolver.settings()))
{
for rule in [ for rule in [
// The formatter might collapse implicit string concatenation on a single line. // The formatter might collapse implicit string concatenation on a single line.
Rule::SingleLineImplicitStringConcatenation, Rule::SingleLineImplicitStringConcatenation,
@ -760,9 +748,7 @@ pub(super) fn warn_incompatible_formatter_settings(
} }
// Next, validate settings-specific incompatibilities. // Next, validate settings-specific incompatibilities.
for setting in std::iter::once(&pyproject_config.settings) for setting in resolver.settings() {
.chain(resolver.iter().flat_map(|resolver| resolver.settings()))
{
// Validate all rules that rely on tab styles. // Validate all rules that rely on tab styles.
if setting.linter.rules.enabled(Rule::TabIndentation) if setting.linter.rules.enabled(Rule::TabIndentation)
&& setting.formatter.indent_style.is_tab() && setting.formatter.indent_style.is_tab()

View file

@ -6,7 +6,7 @@ use log::error;
use ruff_linter::source_kind::SourceKind; use ruff_linter::source_kind::SourceKind;
use ruff_python_ast::{PySourceType, SourceType}; 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 ruff_workspace::FormatterSettings;
use crate::args::{CliOverrides, FormatArguments}; use crate::args::{CliOverrides, FormatArguments};
@ -27,24 +27,23 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
cli.stdin_filename.as_deref(), 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); 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 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() { if mode.is_write() {
parrot_stdin()?; parrot_stdin()?;
} }
return Ok(ExitStatus::Success); return Ok(ExitStatus::Success);
} }
let format_settings = &pyproject_config.settings.formatter; if filename.file_name().is_some_and(|name| {
if filename match_exclusion(filename, name, &resolver.base_settings().formatter.exclude)
.file_name() }) {
.is_some_and(|name| match_exclusion(filename, name, &format_settings.exclude))
{
if mode.is_write() { if mode.is_write() {
parrot_stdin()?; parrot_stdin()?;
} }
@ -63,12 +62,7 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &CliOverrides) -> R
}; };
// Format the file. // Format the file.
match format_source_code( match format_source_code(path, &resolver.base_settings().formatter, source_type, mode) {
path,
&pyproject_config.settings.formatter,
source_type,
mode,
) {
Ok(result) => match mode { Ok(result) => match mode {
FormatMode::Write => Ok(ExitStatus::Success), FormatMode::Write => Ok(ExitStatus::Success),
FormatMode::Check | FormatMode::Diff => { FormatMode::Check | FormatMode::Diff => {

View file

@ -29,7 +29,7 @@ pub(crate) fn show_settings(
bail!("No files found under the given path"); 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:?}")?; writeln!(writer, "Resolved settings for: {path:?}")?;
if let Some(settings_path) = pyproject_config.path.as_ref() { if let Some(settings_path) = pyproject_config.path.as_ref() {

View file

@ -27,7 +27,7 @@ use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter; 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_cli::resolve::resolve;
use ruff_formatter::{FormatError, LineWidth, PrintError}; use ruff_formatter::{FormatError, LineWidth, PrintError};
use ruff_linter::logging::LogLevel; use ruff_linter::logging::LogLevel;
@ -38,24 +38,24 @@ use ruff_python_formatter::{
use ruff_python_parser::ParseError; use ruff_python_parser::ParseError;
use ruff_workspace::resolver::{python_files_in_path, PyprojectConfig, ResolvedFile, Resolver}; 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`. fn parse_cli(dirs: &[PathBuf]) -> anyhow::Result<(FormatArguments, CliOverrides)> {
#[allow(clippy::type_complexity)]
fn ruff_check_paths(
dirs: &[PathBuf],
) -> anyhow::Result<(
Vec<Result<ResolvedFile, ignore::Error>>,
Resolver,
PyprojectConfig,
)> {
let args_matches = FormatCommand::command() let args_matches = FormatCommand::command()
.no_binary_name(true) .no_binary_name(true)
.get_matches_from(dirs); .get_matches_from(dirs);
let arguments: FormatCommand = FormatCommand::from_arg_matches(&args_matches)?; let arguments: FormatCommand = FormatCommand::from_arg_matches(&args_matches)?;
let (cli, overrides) = arguments.partition(); 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<PyprojectConfig> {
let mut pyproject_config = resolve( let mut pyproject_config = resolve(
cli.isolated, cli.isolated,
cli.config.as_deref(), cli.config.as_deref(),
&overrides, overrides,
cli.stdin_filename.as_deref(), cli.stdin_filename.as_deref(),
)?; )?;
// We don't want to format pyproject.toml // We don't want to format pyproject.toml
@ -64,11 +64,18 @@ fn ruff_check_paths(
FilePattern::Builtin("*.pyi"), FilePattern::Builtin("*.pyi"),
]) ])
.unwrap(); .unwrap();
let (paths, resolver) = python_files_in_path(&cli.files, &pyproject_config, &overrides)?; Ok(pyproject_config)
if paths.is_empty() {
bail!("no python files in {:?}", dirs)
} }
Ok((paths, resolver, 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<Result<ResolvedFile, ignore::Error>>, 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 /// 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() 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 // Find files to check (or in this case, format twice). Adapted from ruff_cli
// First argument is ignored // 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 results = {
let pb_span = let pb_span =
@ -469,14 +482,7 @@ fn format_dev_project(
#[cfg(feature = "singlethreaded")] #[cfg(feature = "singlethreaded")]
let iter = { paths.into_iter() }; let iter = { paths.into_iter() };
iter.map(|path| { iter.map(|path| {
let result = format_dir_entry( let result = format_dir_entry(path, stability_check, write, &black_options, &resolver);
path,
stability_check,
write,
&black_options,
&resolver,
&pyproject_config,
);
pb_span.pb_inc(1); pb_span.pb_inc(1);
result 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( fn format_dir_entry(
resolved_file: Result<ResolvedFile, ignore::Error>, resolved_file: Result<ResolvedFile, ignore::Error>,
stability_check: bool, stability_check: bool,
write: bool, write: bool,
options: &BlackOptions, options: &BlackOptions,
resolver: &Resolver, resolver: &Resolver,
pyproject_config: &PyprojectConfig,
) -> anyhow::Result<(Result<Statistics, CheckFileError>, PathBuf), Error> { ) -> anyhow::Result<(Result<Statistics, CheckFileError>, PathBuf), Error> {
let resolved_file = resolved_file.context("Iterating the files in the repository failed")?; let resolved_file = resolved_file.context("Iterating the files in the repository failed")?;
// For some reason it does not filter in the beginning // 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 path = resolved_file.into_path();
let mut options = options.to_py_format_options(&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 // 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() { if settings.formatter.line_width != LineWidth::default() {
options = options.with_line_width(settings.formatter.line_width); options = options.with_line_width(settings.formatter.line_width);

View file

@ -11,7 +11,7 @@ use anyhow::Result;
use anyhow::{anyhow, bail}; use anyhow::{anyhow, bail};
use globset::{Candidate, GlobSet}; use globset::{Candidate, GlobSet};
use ignore::{WalkBuilder, WalkState}; use ignore::{WalkBuilder, WalkState};
use itertools::Itertools; use itertools::{Either, Itertools};
use log::debug; use log::debug;
use path_absolutize::path_dedot; use path_absolutize::path_dedot;
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
@ -25,6 +25,7 @@ use crate::pyproject::settings_toml;
use crate::settings::Settings; use crate::settings::Settings;
/// The configuration information from a `pyproject.toml` file. /// The configuration information from a `pyproject.toml` file.
#[derive(Debug)]
pub struct PyprojectConfig { pub struct PyprojectConfig {
/// The strategy used to discover the relevant `pyproject.toml` file for /// The strategy used to discover the relevant `pyproject.toml` file for
/// each Python file. /// each Python file.
@ -63,10 +64,12 @@ pub enum PyprojectDiscoveryStrategy {
} }
impl PyprojectDiscoveryStrategy { impl PyprojectDiscoveryStrategy {
#[inline]
pub const fn is_fixed(self) -> bool { pub const fn is_fixed(self) -> bool {
matches!(self, PyprojectDiscoveryStrategy::Fixed) matches!(self, PyprojectDiscoveryStrategy::Fixed)
} }
#[inline]
pub const fn is_hierarchical(self) -> bool { pub const fn is_hierarchical(self) -> bool {
matches!(self, PyprojectDiscoveryStrategy::Hierarchical) matches!(self, PyprojectDiscoveryStrategy::Hierarchical)
} }
@ -94,40 +97,68 @@ impl Relativity {
} }
} }
#[derive(Default)] #[derive(Debug)]
pub struct Resolver { pub struct Resolver<'a> {
pyproject_config: &'a PyprojectConfig,
settings: BTreeMap<PathBuf, Settings>, settings: BTreeMap<PathBuf, Settings>,
} }
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. /// Add a resolved [`Settings`] under a given [`PathBuf`] scope.
fn add(&mut self, path: PathBuf, settings: Settings) { fn add(&mut self, path: PathBuf, settings: Settings) {
self.settings.insert(path, settings); self.settings.insert(path, settings);
} }
/// Return the appropriate [`Settings`] for a given [`Path`]. /// Return the appropriate [`Settings`] for a given [`Path`].
pub fn resolve<'a>( pub fn resolve(&self, path: &Path) -> &Settings {
&'a self, match self.pyproject_config.strategy {
path: &Path, PyprojectDiscoveryStrategy::Fixed => &self.pyproject_config.settings,
pyproject_config: &'a PyprojectConfig,
) -> &'a Settings {
match pyproject_config.strategy {
PyprojectDiscoveryStrategy::Fixed => &pyproject_config.settings,
PyprojectDiscoveryStrategy::Hierarchical => self PyprojectDiscoveryStrategy::Hierarchical => self
.settings .settings
.iter() .iter()
.rev() .rev()
.find_map(|(root, settings)| path.starts_with(root).then_some(settings)) .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. /// Return a mapping from Python package to its package root.
pub fn package_roots<'a>( pub fn package_roots(&'a self, files: &[&'a Path]) -> FxHashMap<&'a Path, Option<&'a Path>> {
&'a self,
files: &[&'a Path],
pyproject_config: &'a PyprojectConfig,
) -> FxHashMap<&'a Path, Option<&'a Path>> {
// Pre-populate the module cache, since the list of files could (but isn't // Pre-populate the module cache, since the list of files could (but isn't
// required to) contain some `__init__.py` files. // required to) contain some `__init__.py` files.
let mut package_cache: FxHashMap<&Path, bool> = FxHashMap::default(); 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::Occupied(_) => continue,
std::collections::hash_map::Entry::Vacant(entry) => { std::collections::hash_map::Entry::Vacant(entry) => {
let namespace_packages = if has_namespace_packages { let namespace_packages = if has_namespace_packages {
self.resolve(file, pyproject_config) self.resolve(file).linter.namespace_packages.as_slice()
.linter
.namespace_packages
.as_slice()
} else { } else {
&[] &[]
}; };
@ -176,7 +204,12 @@ impl Resolver {
/// Return an iterator over the resolved [`Settings`] in this [`Resolver`]. /// Return an iterator over the resolved [`Settings`] in this [`Resolver`].
pub fn settings(&self) -> impl Iterator<Item = &Settings> { pub fn settings(&self) -> impl Iterator<Item = &Settings> {
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. /// 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], paths: &[PathBuf],
pyproject_config: &PyprojectConfig, pyproject_config: &'a PyprojectConfig,
transformer: &dyn ConfigurationTransformer, transformer: &dyn ConfigurationTransformer,
) -> Result<(Vec<Result<ResolvedFile, ignore::Error>>, Resolver)> { ) -> Result<(Vec<Result<ResolvedFile, ignore::Error>>, Resolver<'a>)> {
// Normalize every path (e.g., convert from relative to absolute). // Normalize every path (e.g., convert from relative to absolute).
let mut paths: Vec<PathBuf> = paths.iter().map(fs::normalize_path).unique().collect(); let mut paths: Vec<PathBuf> = paths.iter().map(fs::normalize_path).unique().collect();
// Search for `pyproject.toml` files in all parent directories. // 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(); let mut seen = FxHashSet::default();
if pyproject_config.strategy.is_hierarchical() { if resolver.is_hierarchical() {
for path in &paths { for path in &paths {
for ancestor in path.ancestors() { for ancestor in path.ancestors() {
if seen.insert(ancestor) { if seen.insert(ancestor) {
@ -315,8 +348,8 @@ pub fn python_files_in_path(
} }
// Check if the paths themselves are excluded. // Check if the paths themselves are excluded.
if pyproject_config.settings.file_resolver.force_exclude { if resolver.force_exclude() {
paths.retain(|path| !is_file_excluded(path, &resolver, pyproject_config)); paths.retain(|path| !is_file_excluded(path, &resolver));
if paths.is_empty() { if paths.is_empty() {
return Ok((vec![], resolver)); return Ok((vec![], resolver));
} }
@ -330,11 +363,12 @@ pub fn python_files_in_path(
for path in rest_paths { for path in rest_paths {
builder.add(path); builder.add(path);
} }
builder.standard_filters(pyproject_config.settings.file_resolver.respect_gitignore); builder.standard_filters(resolver.respect_gitignore());
builder.hidden(false); builder.hidden(false);
let walker = builder.build_parallel(); let walker = builder.build_parallel();
// Run the `WalkParallel` to collect all Python files. // Run the `WalkParallel` to collect all Python files.
let is_hierarchical = resolver.is_hierarchical();
let error: std::sync::Mutex<Result<()>> = std::sync::Mutex::new(Ok(())); let error: std::sync::Mutex<Result<()>> = std::sync::Mutex::new(Ok(()));
let resolver: RwLock<Resolver> = RwLock::new(resolver); let resolver: RwLock<Resolver> = RwLock::new(resolver);
let files: std::sync::Mutex<Vec<Result<ResolvedFile, ignore::Error>>> = let files: std::sync::Mutex<Vec<Result<ResolvedFile, ignore::Error>>> =
@ -346,7 +380,7 @@ pub fn python_files_in_path(
if entry.depth() > 0 { if entry.depth() > 0 {
let path = entry.path(); let path = entry.path();
let resolver = resolver.read().unwrap(); 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() { if let Some(file_name) = path.file_name() {
let file_path = Candidate::new(path); let file_path = Candidate::new(path);
let file_basename = Candidate::new(file_name); 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 // Search for the `pyproject.toml` file in this directory, before we visit any
// of its contents. // of its contents.
if pyproject_config.strategy.is_hierarchical() { if is_hierarchical {
if let Ok(entry) = &result { if let Ok(entry) = &result {
if entry if entry
.file_type() .file_type()
@ -416,7 +450,7 @@ pub fn python_files_in_path(
// Otherwise, check if the file is included. // Otherwise, check if the file is included.
let path = entry.path(); let path = entry.path();
let resolver = resolver.read().unwrap(); 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) { if settings.file_resolver.include.is_match(path) {
debug!("Included path via `include`: {:?}", path); debug!("Included path via `include`: {:?}", path);
Some(ResolvedFile::Nested(entry.into_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. /// Return `true` if the Python file at [`Path`] is _not_ excluded.
pub fn python_file_at_path( pub fn python_file_at_path(
path: &Path, path: &Path,
pyproject_config: &PyprojectConfig, resolver: &mut Resolver,
transformer: &dyn ConfigurationTransformer, transformer: &dyn ConfigurationTransformer,
) -> Result<bool> { ) -> Result<bool> {
// Normalize the path (e.g., convert from relative to absolute). // Normalize the path (e.g., convert from relative to absolute).
let path = fs::normalize_path(path); let path = fs::normalize_path(path);
// Search for `pyproject.toml` files in all parent directories. // Search for `pyproject.toml` files in all parent directories.
let mut resolver = Resolver::default(); if resolver.is_hierarchical() {
if pyproject_config.strategy.is_hierarchical() {
for ancestor in path.ancestors() { for ancestor in path.ancestors() {
if let Some(pyproject) = settings_toml(ancestor)? { if let Some(pyproject) = settings_toml(ancestor)? {
let (root, settings) = let (root, settings) =
@ -514,18 +547,14 @@ pub fn python_file_at_path(
} }
// Check exclusions. // 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. /// Return `true` if the given top-level [`Path`] should be excluded.
fn is_file_excluded( fn is_file_excluded(path: &Path, resolver: &Resolver) -> bool {
path: &Path,
resolver: &Resolver,
pyproject_strategy: &PyprojectConfig,
) -> bool {
// TODO(charlie): Respect gitignore. // TODO(charlie): Respect gitignore.
for path in path.ancestors() { 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() { if let Some(file_name) = path.file_name() {
let file_path = Candidate::new(path); let file_path = Candidate::new(path);
let file_basename = Candidate::new(file_name); let file_basename = Candidate::new(file_name);
@ -618,7 +647,6 @@ mod tests {
#[test] #[test]
fn rooted_exclusion() -> Result<()> { fn rooted_exclusion() -> Result<()> {
let package_root = test_resource_path("package"); let package_root = test_resource_path("package");
let resolver = Resolver::default();
let pyproject_config = PyprojectConfig::new( let pyproject_config = PyprojectConfig::new(
PyprojectDiscoveryStrategy::Hierarchical, PyprojectDiscoveryStrategy::Hierarchical,
resolve_root_settings( resolve_root_settings(
@ -628,20 +656,19 @@ mod tests {
)?, )?,
None, None,
); );
let resolver = Resolver::new(&pyproject_config);
// src/app.py should not be excluded even if it lives in a hierarchy that should // 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 // be excluded by virtue of the pyproject.toml having `resources/*` in
// it. // it.
assert!(!is_file_excluded( assert!(!is_file_excluded(
&package_root.join("src/app.py"), &package_root.join("src/app.py"),
&resolver, &resolver,
&pyproject_config,
)); ));
// However, resources/ignored.py should be ignored, since that `resources` is // However, resources/ignored.py should be ignored, since that `resources` is
// beneath the package root. // beneath the package root.
assert!(is_file_excluded( assert!(is_file_excluded(
&package_root.join("resources/ignored.py"), &package_root.join("resources/ignored.py"),
&resolver, &resolver,
&pyproject_config,
)); ));
Ok(()) Ok(())
} }