mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-24 13:33:50 +00:00
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:
parent
79f4abbb8d
commit
4a3bb67b5f
9 changed files with 144 additions and 149 deletions
|
@ -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<T> PackageCaches for Option<T>
|
||||
|
@ -469,28 +468,18 @@ 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())
|
||||
{
|
||||
for settings in resolver.settings() {
|
||||
init_cache(&settings.cache_dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self(
|
||||
package_roots
|
||||
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -38,7 +38,6 @@ pub(crate) fn add_noqa(
|
|||
.flatten()
|
||||
.map(ResolvedFile::path)
|
||||
.collect::<Vec<_>>(),
|
||||
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,
|
||||
|
|
|
@ -57,16 +57,11 @@ pub(crate) fn check(
|
|||
.flatten()
|
||||
.map(ResolvedFile::path)
|
||||
.collect::<Vec<_>>(),
|
||||
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();
|
||||
|
|
|
@ -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<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 !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,
|
||||
)?;
|
||||
|
|
|
@ -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::<Vec<_>>(),
|
||||
&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()
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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<Result<ResolvedFile, ignore::Error>>,
|
||||
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<PyprojectConfig> {
|
||||
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(pyproject_config)
|
||||
}
|
||||
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
|
||||
|
@ -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<ResolvedFile, ignore::Error>,
|
||||
stability_check: bool,
|
||||
write: bool,
|
||||
options: &BlackOptions,
|
||||
resolver: &Resolver,
|
||||
pyproject_config: &PyprojectConfig,
|
||||
) -> anyhow::Result<(Result<Statistics, CheckFileError>, 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);
|
||||
|
|
|
@ -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<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.
|
||||
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<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.
|
||||
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<Result<ResolvedFile, ignore::Error>>, Resolver)> {
|
||||
) -> Result<(Vec<Result<ResolvedFile, ignore::Error>>, Resolver<'a>)> {
|
||||
// Normalize every path (e.g., convert from relative to absolute).
|
||||
let mut paths: Vec<PathBuf> = 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<Result<()>> = std::sync::Mutex::new(Ok(()));
|
||||
let resolver: RwLock<Resolver> = RwLock::new(resolver);
|
||||
let files: std::sync::Mutex<Vec<Result<ResolvedFile, ignore::Error>>> =
|
||||
|
@ -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<bool> {
|
||||
// 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(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue