Simplify some logic around configuration detection (#1197)

This commit is contained in:
Charlie Marsh 2022-12-12 10:15:05 -05:00 committed by GitHub
parent 73794fc299
commit ac6fa1dc88
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 147 additions and 158 deletions

View file

@ -198,6 +198,7 @@ pub struct Arguments {
} }
/// CLI settings that function as configuration overrides. /// CLI settings that function as configuration overrides.
#[derive(Clone)]
#[allow(clippy::struct_excessive_bools)] #[allow(clippy::struct_excessive_bools)]
pub struct Overrides { pub struct Overrides {
pub dummy_variable_rgx: Option<Regex>, pub dummy_variable_rgx: Option<Regex>,

View file

@ -11,20 +11,15 @@ use crate::settings::types::SerializationFormat;
use crate::{Configuration, Settings}; use crate::{Configuration, Settings};
/// Print the user-facing configuration settings. /// Print the user-facing configuration settings.
pub fn show_settings( pub fn show_settings(configuration: &Configuration, pyproject: Option<&Path>) {
configuration: &Configuration,
project_root: Option<&Path>,
pyproject: Option<&Path>,
) {
println!("Resolved configuration: {configuration:#?}"); println!("Resolved configuration: {configuration:#?}");
println!("Found project root at: {project_root:?}");
println!("Found pyproject.toml at: {pyproject:?}"); println!("Found pyproject.toml at: {pyproject:?}");
} }
/// Show the list of files to be checked based on current settings. /// Show the list of files to be checked based on current settings.
pub fn show_files(files: &[PathBuf], default: &Settings, overrides: &Overrides) { pub fn show_files(files: &[PathBuf], defaults: &Settings, overrides: &Overrides) {
// Collect all files in the hierarchy. // Collect all files in the hierarchy.
let (paths, _resolver) = collect_python_files(files, overrides, default); let (paths, _resolver) = collect_python_files(files, overrides, defaults);
// Print the list of files. // Print the list of files.
for entry in paths for entry in paths

View file

@ -84,24 +84,22 @@ pub mod updates;
mod vendored; mod vendored;
pub mod visibility; pub mod visibility;
/// Run Ruff over Python source code directly. fn resolve(path: &Path) -> Result<Settings> {
pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Check>> { // Find the relevant `pyproject.toml`.
// Find the project root and pyproject.toml. let Some(pyproject) = pyproject::find_pyproject_toml(path) else {
let project_root = pyproject::find_project_root(&[path.to_path_buf()]); debug!("Unable to find pyproject.toml; using default settings...");
match &project_root { return Settings::from_configuration(Configuration::default(), None);
Some(path) => debug!("Found project root at: {:?}", path),
None => debug!("Unable to identify project root; assuming current directory..."),
};
let pyproject = pyproject::find_pyproject_toml(project_root.as_ref());
match &pyproject {
Some(path) => debug!("Found pyproject.toml at: {:?}", path),
None => debug!("Unable to find pyproject.toml; using default settings..."),
}; };
let settings = Settings::from_configuration( // Load and parse the `pyproject.toml`.
Configuration::from_pyproject(pyproject.as_ref())?, let options = pyproject::load_options(&pyproject)?;
project_root.as_deref(), let configuration = Configuration::from_options(options)?;
)?; Settings::from_configuration(configuration, pyproject.parent())
}
/// Run Ruff over Python source code directly.
pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Check>> {
let settings = resolve(path)?;
// Tokenize once. // Tokenize once.
let tokens: Vec<LexResult> = tokenize(contents); let tokens: Vec<LexResult> = tokenize(contents);

View file

@ -31,12 +31,13 @@ use ::ruff::settings::types::SerializationFormat;
use ::ruff::settings::{pyproject, Settings}; use ::ruff::settings::{pyproject, Settings};
#[cfg(feature = "update-informer")] #[cfg(feature = "update-informer")]
use ::ruff::updates; use ::ruff::updates;
use ::ruff::{cache, commands, fs}; use ::ruff::{cache, commands};
use anyhow::Result; use anyhow::Result;
use clap::{CommandFactory, Parser}; use clap::{CommandFactory, Parser};
use colored::Colorize; use colored::Colorize;
use log::{debug, error}; use log::{debug, error};
use notify::{recommended_watcher, RecursiveMode, Watcher}; use notify::{recommended_watcher, RecursiveMode, Watcher};
use path_absolutize::path_dedot;
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
use rayon::prelude::*; use rayon::prelude::*;
use rustpython_ast::Location; use rustpython_ast::Location;
@ -60,14 +61,14 @@ fn run_once_stdin(
fn run_once( fn run_once(
files: &[PathBuf], files: &[PathBuf],
default: &Settings, defaults: &Settings,
overrides: &Overrides, overrides: &Overrides,
cache: bool, cache: bool,
autofix: &fixer::Mode, autofix: &fixer::Mode,
) -> Diagnostics { ) -> Diagnostics {
// Collect all the files to check. // Collect all the files to check.
let start = Instant::now(); let start = Instant::now();
let (paths, resolver) = collect_python_files(files, overrides, default); let (paths, resolver) = collect_python_files(files, overrides, defaults);
let duration = start.elapsed(); let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration); debug!("Identified files to lint in: {:?}", duration);
@ -77,7 +78,7 @@ fn run_once(
match entry { match entry {
Ok(entry) => { Ok(entry) => {
let path = entry.path(); let path = entry.path();
let settings = resolver.resolve(path).unwrap_or(default); let settings = resolver.resolve(path).unwrap_or(defaults);
lint_path(path, settings, &cache.into(), autofix) lint_path(path, settings, &cache.into(), autofix)
.map_err(|e| (Some(path.to_owned()), e.to_string())) .map_err(|e| (Some(path.to_owned()), e.to_string()))
} }
@ -89,7 +90,7 @@ fn run_once(
} }
.unwrap_or_else(|(path, message)| { .unwrap_or_else(|(path, message)| {
if let Some(path) = &path { if let Some(path) = &path {
let settings = resolver.resolve(path).unwrap_or(default); let settings = resolver.resolve(path).unwrap_or(defaults);
if settings.enabled.contains(&CheckCode::E902) { if settings.enabled.contains(&CheckCode::E902) {
Diagnostics::new(vec![Message { Diagnostics::new(vec![Message {
kind: CheckKind::IOError(message), kind: CheckKind::IOError(message),
@ -121,10 +122,10 @@ fn run_once(
diagnostics diagnostics
} }
fn add_noqa(files: &[PathBuf], default: &Settings, overrides: &Overrides) -> usize { fn add_noqa(files: &[PathBuf], defaults: &Settings, overrides: &Overrides) -> usize {
// Collect all the files to check. // Collect all the files to check.
let start = Instant::now(); let start = Instant::now();
let (paths, resolver) = collect_python_files(files, overrides, default); let (paths, resolver) = collect_python_files(files, overrides, defaults);
let duration = start.elapsed(); let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration); debug!("Identified files to lint in: {:?}", duration);
@ -133,7 +134,7 @@ fn add_noqa(files: &[PathBuf], default: &Settings, overrides: &Overrides) -> usi
.flatten() .flatten()
.filter_map(|entry| { .filter_map(|entry| {
let path = entry.path(); let path = entry.path();
let settings = resolver.resolve(path).unwrap_or(default); let settings = resolver.resolve(path).unwrap_or(defaults);
match add_noqa_to_path(path, settings) { match add_noqa_to_path(path, settings) {
Ok(count) => Some(count), Ok(count) => Some(count),
Err(e) => { Err(e) => {
@ -150,10 +151,10 @@ fn add_noqa(files: &[PathBuf], default: &Settings, overrides: &Overrides) -> usi
modifications modifications
} }
fn autoformat(files: &[PathBuf], default: &Settings, overrides: &Overrides) -> usize { fn autoformat(files: &[PathBuf], defaults: &Settings, overrides: &Overrides) -> usize {
// Collect all the files to format. // Collect all the files to format.
let start = Instant::now(); let start = Instant::now();
let (paths, resolver) = collect_python_files(files, overrides, default); let (paths, resolver) = collect_python_files(files, overrides, defaults);
let duration = start.elapsed(); let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration); debug!("Identified files to lint in: {:?}", duration);
@ -162,7 +163,7 @@ fn autoformat(files: &[PathBuf], default: &Settings, overrides: &Overrides) -> u
.flatten() .flatten()
.filter_map(|entry| { .filter_map(|entry| {
let path = entry.path(); let path = entry.path();
let settings = resolver.resolve(path).unwrap_or(default); let settings = resolver.resolve(path).unwrap_or(defaults);
match autoformat_path(path, settings) { match autoformat_path(path, settings) {
Ok(()) => Some(()), Ok(()) => Some(()),
Err(e) => { Err(e) => {
@ -185,50 +186,36 @@ fn inner_main() -> Result<ExitCode> {
let log_level = extract_log_level(&cli); let log_level = extract_log_level(&cli);
set_up_logging(&log_level)?; set_up_logging(&log_level)?;
if cli.show_settings && cli.show_files {
anyhow::bail!("specify --show-settings or show-files (not both)")
}
if let Some(shell) = cli.generate_shell_completion { if let Some(shell) = cli.generate_shell_completion {
shell.generate(&mut Cli::command(), &mut io::stdout()); shell.generate(&mut Cli::command(), &mut io::stdout());
return Ok(ExitCode::SUCCESS); return Ok(ExitCode::SUCCESS);
} }
// Find the project root and pyproject.toml. // Find the `pyproject.toml`.
// TODO(charlie): look in the current directory, but respect `--config`.
let project_root = cli.config.as_ref().map_or_else(
|| pyproject::find_project_root(&cli.files),
|config| config.parent().map(fs::normalize_path),
);
let pyproject = cli let pyproject = cli
.config .config
.or_else(|| pyproject::find_pyproject_toml(project_root.as_ref())); .or_else(|| pyproject::find_pyproject_toml(&path_dedot::CWD));
match &project_root {
Some(path) => debug!("Found project root at: {:?}", path),
None => debug!("Unable to identify project root; assuming current directory..."),
};
match &pyproject {
Some(path) => debug!("Found pyproject.toml at: {:?}", path),
None => debug!("Unable to find pyproject.toml; using default settings..."),
};
// Reconcile configuration from pyproject.toml and command-line arguments. // Reconcile configuration from `pyproject.toml` and command-line arguments.
let mut configuration = Configuration::from_pyproject(pyproject.as_ref())?; let mut configuration = pyproject
configuration.merge(&overrides); .as_ref()
.map(|path| Configuration::from_pyproject(path))
if cli.show_settings && cli.show_files { .transpose()?
eprintln!("Error: specify --show-settings or show-files (not both)."); .unwrap_or_default();
return Ok(ExitCode::FAILURE); configuration.merge(overrides.clone());
}
if cli.show_settings { if cli.show_settings {
// TODO(charlie): This would be more useful if required a single file, and told // TODO(charlie): This would be more useful if required a single file, and told
// you the settings used to lint that file. // you the settings used to lint that file.
commands::show_settings( commands::show_settings(&configuration, pyproject.as_deref());
&configuration,
project_root.as_deref(),
pyproject.as_deref(),
);
return Ok(ExitCode::SUCCESS); return Ok(ExitCode::SUCCESS);
} }
// TODO(charlie): Included in `pyproject.toml`, but not inherited. // Extract options that are included in the `pyproject.toml`, but aren't in
// `Settings`.
let fix = if configuration.fix { let fix = if configuration.fix {
fixer::Mode::Apply fixer::Mode::Apply
} else if matches!(configuration.format, SerializationFormat::Json) { } else if matches!(configuration.format, SerializationFormat::Json) {
@ -238,7 +225,12 @@ fn inner_main() -> Result<ExitCode> {
}; };
let format = configuration.format; let format = configuration.format;
let settings = Settings::from_configuration(configuration, project_root.as_deref())?; // Construct the "default" settings. These are used when no `pyproject.toml`
// files are present, or files are injected from outside of the hierarchy.
let defaults = Settings::from_configuration(
configuration,
pyproject.as_ref().and_then(|path| path.parent()),
)?;
if let Some(code) = cli.explain { if let Some(code) = cli.explain {
commands::explain(&code, format)?; commands::explain(&code, format)?;
@ -246,7 +238,7 @@ fn inner_main() -> Result<ExitCode> {
} }
if cli.show_files { if cli.show_files {
commands::show_files(&cli.files, &settings, &overrides); commands::show_files(&cli.files, &defaults, &overrides);
return Ok(ExitCode::SUCCESS); return Ok(ExitCode::SUCCESS);
} }
@ -278,7 +270,7 @@ fn inner_main() -> Result<ExitCode> {
let messages = run_once( let messages = run_once(
&cli.files, &cli.files,
&settings, &defaults,
&overrides, &overrides,
cache_enabled, cache_enabled,
&fixer::Mode::None, &fixer::Mode::None,
@ -294,8 +286,8 @@ fn inner_main() -> Result<ExitCode> {
loop { loop {
match rx.recv() { match rx.recv() {
Ok(e) => { Ok(event) => {
let paths = e?.paths; let paths = event?.paths;
let py_changed = paths.iter().any(|p| { let py_changed = paths.iter().any(|p| {
p.extension() p.extension()
.map(|ext| ext == "py" || ext == "pyi") .map(|ext| ext == "py" || ext == "pyi")
@ -307,7 +299,7 @@ fn inner_main() -> Result<ExitCode> {
let messages = run_once( let messages = run_once(
&cli.files, &cli.files,
&settings, &defaults,
&overrides, &overrides,
cache_enabled, cache_enabled,
&fixer::Mode::None, &fixer::Mode::None,
@ -315,16 +307,16 @@ fn inner_main() -> Result<ExitCode> {
printer.write_continuously(&messages)?; printer.write_continuously(&messages)?;
} }
} }
Err(e) => return Err(e.into()), Err(err) => return Err(err.into()),
} }
} }
} else if cli.add_noqa { } else if cli.add_noqa {
let modifications = add_noqa(&cli.files, &settings, &overrides); let modifications = add_noqa(&cli.files, &defaults, &overrides);
if modifications > 0 && log_level >= LogLevel::Default { if modifications > 0 && log_level >= LogLevel::Default {
println!("Added {modifications} noqa directives."); println!("Added {modifications} noqa directives.");
} }
} else if cli.autoformat { } else if cli.autoformat {
let modifications = autoformat(&cli.files, &settings, &overrides); let modifications = autoformat(&cli.files, &defaults, &overrides);
if modifications > 0 && log_level >= LogLevel::Default { if modifications > 0 && log_level >= LogLevel::Default {
println!("Formatted {modifications} files."); println!("Formatted {modifications} files.");
} }
@ -335,9 +327,9 @@ fn inner_main() -> Result<ExitCode> {
let diagnostics = if is_stdin { let diagnostics = if is_stdin {
let filename = cli.stdin_filename.unwrap_or_else(|| "-".to_string()); let filename = cli.stdin_filename.unwrap_or_else(|| "-".to_string());
let path = Path::new(&filename); let path = Path::new(&filename);
run_once_stdin(&settings, path, &fix)? run_once_stdin(&defaults, path, &fix)?
} else { } else {
run_once(&cli.files, &settings, &overrides, cache_enabled, &fix) run_once(&cli.files, &defaults, &overrides, cache_enabled, &fix)
}; };
// Always try to print violations (the printer itself may suppress output), // Always try to print violations (the printer itself may suppress output),

View file

@ -42,7 +42,7 @@ pub fn settings_for_path(pyproject: &Path, overrides: &Overrides) -> Result<(Pat
.to_path_buf(); .to_path_buf();
let options = pyproject::load_options(pyproject)?; let options = pyproject::load_options(pyproject)?;
let mut configuration = Configuration::from_options(options)?; let mut configuration = Configuration::from_options(options)?;
configuration.merge(overrides); configuration.merge(overrides.clone());
let settings = Settings::from_configuration(configuration, Some(&project_root))?; let settings = Settings::from_configuration(configuration, Some(&project_root))?;
Ok((project_root, settings)) Ok((project_root, settings))
} }

View file

@ -5,7 +5,6 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use log::debug;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
@ -81,15 +80,8 @@ static DEFAULT_DUMMY_VARIABLE_RGX: Lazy<Regex> =
Lazy::new(|| Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap()); Lazy::new(|| Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap());
impl Configuration { impl Configuration {
pub fn from_pyproject(pyproject: Option<&PathBuf>) -> Result<Self> { pub fn from_pyproject(pyproject: &Path) -> Result<Self> {
Self::from_options(pyproject.map_or_else( Self::from_options(load_options(pyproject)?)
|| {
debug!("No pyproject.toml found.");
debug!("Falling back to default configuration...");
Ok(Options::default())
},
|path| load_options(path),
)?)
} }
pub fn from_options(options: Options) -> Result<Self> { pub fn from_options(options: Options) -> Result<Self> {
@ -184,54 +176,90 @@ impl Configuration {
}) })
} }
pub fn merge(&mut self, overrides: &Overrides) { pub fn merge(&mut self, overrides: Overrides) {
if let Some(dummy_variable_rgx) = &overrides.dummy_variable_rgx { if let Some(dummy_variable_rgx) = overrides.dummy_variable_rgx {
self.dummy_variable_rgx = dummy_variable_rgx.clone(); self.dummy_variable_rgx = dummy_variable_rgx;
} }
if let Some(exclude) = &overrides.exclude { if let Some(exclude) = overrides.exclude {
self.exclude = exclude.clone(); self.exclude = exclude;
} }
if let Some(extend_exclude) = &overrides.extend_exclude { if let Some(extend_exclude) = overrides.extend_exclude {
self.extend_exclude = extend_exclude.clone(); self.extend_exclude = extend_exclude;
} }
if let Some(extend_ignore) = &overrides.extend_ignore { if let Some(extend_ignore) = overrides.extend_ignore {
self.extend_ignore = extend_ignore.clone(); self.extend_ignore = extend_ignore;
} }
if let Some(extend_select) = &overrides.extend_select { if let Some(extend_select) = overrides.extend_select {
self.extend_select = extend_select.clone(); self.extend_select = extend_select;
} }
if let Some(fix) = &overrides.fix { if let Some(fix) = overrides.fix {
self.fix = *fix; self.fix = fix;
} }
if let Some(fixable) = &overrides.fixable { if let Some(fixable) = overrides.fixable {
self.fixable = fixable.clone(); self.fixable = fixable;
} }
if let Some(format) = &overrides.format { if let Some(format) = overrides.format {
self.format = *format; self.format = format;
} }
if let Some(ignore) = &overrides.ignore { if let Some(ignore) = overrides.ignore {
self.ignore = ignore.clone(); self.ignore = ignore;
} }
if let Some(line_length) = &overrides.line_length { if let Some(line_length) = overrides.line_length {
self.line_length = *line_length; self.line_length = line_length;
} }
if let Some(max_complexity) = &overrides.max_complexity { if let Some(max_complexity) = overrides.max_complexity {
self.mccabe.max_complexity = *max_complexity; self.mccabe.max_complexity = max_complexity;
} }
if let Some(per_file_ignores) = &overrides.per_file_ignores { if let Some(per_file_ignores) = overrides.per_file_ignores {
self.per_file_ignores = collect_per_file_ignores(per_file_ignores.clone()); self.per_file_ignores = collect_per_file_ignores(per_file_ignores);
} }
if let Some(select) = &overrides.select { if let Some(select) = overrides.select {
self.select = select.clone(); self.select = select;
} }
if let Some(show_source) = &overrides.show_source { if let Some(show_source) = overrides.show_source {
self.show_source = *show_source; self.show_source = show_source;
} }
if let Some(target_version) = &overrides.target_version { if let Some(target_version) = overrides.target_version {
self.target_version = *target_version; self.target_version = target_version;
} }
if let Some(unfixable) = &overrides.unfixable { if let Some(unfixable) = overrides.unfixable {
self.unfixable = unfixable.clone(); self.unfixable = unfixable;
}
}
}
impl Default for Configuration {
fn default() -> Self {
Configuration {
allowed_confusables: FxHashSet::default(),
dummy_variable_rgx: DEFAULT_DUMMY_VARIABLE_RGX.clone(),
src: vec![Path::new(".").to_path_buf()],
target_version: PythonVersion::Py310,
exclude: DEFAULT_EXCLUDE.clone(),
extend_exclude: Vec::default(),
extend_ignore: Vec::default(),
select: vec![CheckCodePrefix::E, CheckCodePrefix::F],
extend_select: Vec::default(),
external: Vec::default(),
fix: false,
fixable: CATEGORIES.to_vec(),
unfixable: Vec::default(),
format: SerializationFormat::default(),
ignore: Vec::default(),
ignore_init_module_imports: false,
line_length: 88,
per_file_ignores: Vec::default(),
show_source: false,
// Plugins
flake8_annotations: flake8_annotations::settings::Settings::default(),
flake8_bugbear: flake8_bugbear::settings::Settings::default(),
flake8_import_conventions: flake8_import_conventions::settings::Settings::default(),
flake8_quotes: flake8_quotes::settings::Settings::default(),
flake8_tidy_imports: flake8_tidy_imports::settings::Settings::default(),
isort: isort::settings::Settings::default(),
mccabe: mccabe::settings::Settings::default(),
pep8_naming: pep8_naming::settings::Settings::default(),
pyupgrade: pyupgrade::settings::Settings::default(),
} }
} }
} }

View file

@ -3,8 +3,6 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use common_path::common_path_all;
use path_absolutize::Absolutize;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::fs; use crate::fs;
@ -35,14 +33,14 @@ fn parse_pyproject_toml(path: &Path) -> Result<Pyproject> {
Ok(toml::from_str(&contents)?) Ok(toml::from_str(&contents)?)
} }
pub fn find_pyproject_toml(path: Option<&PathBuf>) -> Option<PathBuf> { /// Find the nearest `pyproject.toml` file.
if let Some(path) = path { pub fn find_pyproject_toml(path: &Path) -> Option<PathBuf> {
let path_pyproject_toml = path.join("pyproject.toml"); for directory in path.ancestors() {
if path_pyproject_toml.is_file() { let pyproject = directory.join("pyproject.toml");
return Some(path_pyproject_toml); if pyproject.is_file() {
return Some(pyproject);
} }
} }
find_user_pyproject_toml() find_user_pyproject_toml()
} }
@ -57,28 +55,6 @@ fn find_user_pyproject_toml() -> Option<PathBuf> {
} }
} }
pub fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
let absolute_sources: Vec<PathBuf> = sources
.iter()
.flat_map(|source| source.absolutize().map(|path| path.to_path_buf()))
.collect();
if let Some(prefix) = common_path_all(absolute_sources.iter().map(PathBuf::as_path)) {
for directory in prefix.ancestors() {
if directory.join(".git").is_dir() {
return Some(directory.to_path_buf());
}
if directory.join(".hg").is_dir() {
return Some(directory.to_path_buf());
}
if directory.join("pyproject.toml").is_file() {
return Some(directory.to_path_buf());
}
}
}
None
}
pub fn load_options(pyproject: &Path) -> Result<Options> { pub fn load_options(pyproject: &Path) -> Result<Options> {
Ok(parse_pyproject_toml(pyproject) Ok(parse_pyproject_toml(pyproject)
.map_err(|err| anyhow!("Failed to parse `{}`: {}", pyproject.to_string_lossy(), err))? .map_err(|err| anyhow!("Failed to parse `{}`: {}", pyproject.to_string_lossy(), err))?
@ -90,7 +66,6 @@ pub fn load_options(pyproject: &Path) -> Result<Options> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::env::current_dir; use std::env::current_dir;
use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use anyhow::Result; use anyhow::Result;
@ -100,7 +75,7 @@ mod tests {
use crate::flake8_quotes::settings::Quote; use crate::flake8_quotes::settings::Quote;
use crate::flake8_tidy_imports::settings::Strictness; use crate::flake8_tidy_imports::settings::Strictness;
use crate::settings::pyproject::{ use crate::settings::pyproject::{
find_project_root, find_pyproject_toml, parse_pyproject_toml, Options, Pyproject, Tools, find_pyproject_toml, parse_pyproject_toml, Options, Pyproject, Tools,
}; };
use crate::settings::types::PatternPrefixPair; use crate::settings::types::PatternPrefixPair;
use crate::{ use crate::{
@ -369,14 +344,14 @@ other-attribute = 1
#[test] #[test]
fn find_and_parse_pyproject_toml() -> Result<()> { fn find_and_parse_pyproject_toml() -> Result<()> {
let cwd = current_dir()?; let cwd = current_dir()?;
let project_root = let pyproject =
find_project_root(&[PathBuf::from("resources/test/fixtures/__init__.py")]).unwrap(); find_pyproject_toml(&cwd.join("resources/test/fixtures/__init__.py")).unwrap();
assert_eq!(project_root, cwd.join("resources/test/fixtures")); assert_eq!(
pyproject,
cwd.join("resources/test/fixtures/pyproject.toml")
);
let path = find_pyproject_toml(Some(&project_root)).unwrap(); let pyproject = parse_pyproject_toml(&pyproject)?;
assert_eq!(path, cwd.join("resources/test/fixtures/pyproject.toml"));
let pyproject = parse_pyproject_toml(&path)?;
let config = pyproject.tool.and_then(|tool| tool.ruff).unwrap(); let config = pyproject.tool.and_then(|tool| tool.ruff).unwrap();
assert_eq!( assert_eq!(
config, config,