From b856b9603f5c3f9249f16a8b13b6fe76c02399cf Mon Sep 17 00:00:00 2001 From: Logan Hunt <39638017+dosisod@users.noreply.github.com> Date: Wed, 6 Sep 2023 13:31:14 -0700 Subject: [PATCH] feat(config): Support for `pyproject.toml` files (#790) This PR adds support for parsing `pyproject.toml` config files. The convention for these files is to put any tooling related configuration into the `tool.NAME` section, so in this case, `tool.typos`. I have verified that the changes are pulled correctly, even if the `tool.typos` section is not present. Closes #361 --- crates/typos-cli/src/bin/typos-cli/main.rs | 12 +++- crates/typos-cli/src/config.rs | 66 +++++++++++++++------- 2 files changed, 56 insertions(+), 22 deletions(-) diff --git a/crates/typos-cli/src/bin/typos-cli/main.rs b/crates/typos-cli/src/bin/typos-cli/main.rs index d7902fb8e..37b996760 100644 --- a/crates/typos-cli/src/bin/typos-cli/main.rs +++ b/crates/typos-cli/src/bin/typos-cli/main.rs @@ -68,7 +68,9 @@ fn run_dump_config(args: &args::Args, output_path: &std::path::Path) -> proc_exi if let Some(path) = args.custom_config.as_ref() { let custom = typos_cli::config::Config::from_file(path) .with_code(proc_exit::sysexits::CONFIG_ERR)?; - overrides.update(&custom); + if let Some(custom) = custom { + overrides.update(&custom); + } } overrides.update(&args.config.to_config()); engine.set_overrides(overrides); @@ -119,7 +121,9 @@ fn run_type_list(args: &args::Args) -> proc_exit::ExitResult { if let Some(path) = args.custom_config.as_ref() { let custom = typos_cli::config::Config::from_file(path) .with_code(proc_exit::sysexits::CONFIG_ERR)?; - overrides.update(&custom); + if let Some(custom) = custom { + overrides.update(&custom); + } } overrides.update(&args.config.to_config()); engine.set_overrides(overrides); @@ -154,7 +158,9 @@ fn run_checks(args: &args::Args) -> proc_exit::ExitResult { if let Some(path) = args.custom_config.as_ref() { let custom = typos_cli::config::Config::from_file(path) .with_code(proc_exit::sysexits::CONFIG_ERR)?; - overrides.update(&custom); + if let Some(custom) = custom { + overrides.update(&custom); + } } overrides.update(&args.config.to_config()); engine.set_overrides(overrides); diff --git a/crates/typos-cli/src/config.rs b/crates/typos-cli/src/config.rs index 7ee6c3e0d..9451aebd6 100644 --- a/crates/typos-cli/src/config.rs +++ b/crates/typos-cli/src/config.rs @@ -4,7 +4,8 @@ use kstring::KString; use crate::file_type_specifics; -pub const SUPPORTED_FILE_NAMES: &[&str] = &["typos.toml", "_typos.toml", ".typos.toml"]; +pub const SUPPORTED_FILE_NAMES: &[&str] = + &["typos.toml", "_typos.toml", ".typos.toml", "pyproject.toml"]; #[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] @@ -19,18 +20,33 @@ pub struct Config { pub overrides: EngineConfig, } +#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(default)] +#[serde(rename_all = "kebab-case")] +pub struct PyprojectTomlConfig { + pub tool: PyprojectTomlTool, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(default)] +#[serde(rename_all = "kebab-case")] +pub struct PyprojectTomlTool { + pub typos: Option, +} + impl Config { pub fn from_dir(cwd: &std::path::Path) -> Result, anyhow::Error> { - let config = if let Some(path) = find_project_file(cwd, SUPPORTED_FILE_NAMES) { - log::debug!("Loading {}", path.display()); - Some(Self::from_file(&path)?) - } else { - None - }; - Ok(config) + for file in find_project_files(cwd, SUPPORTED_FILE_NAMES) { + log::debug!("Loading {}", file.display()); + if let Some(config) = Self::from_file(&file)? { + return Ok(Some(config)); + } + } + + Ok(None) } - pub fn from_file(path: &std::path::Path) -> Result { + pub fn from_file(path: &std::path::Path) -> Result, anyhow::Error> { let s = std::fs::read_to_string(path).map_err(|err| { let kind = err.kind(); std::io::Error::new( @@ -38,7 +54,20 @@ impl Config { format!("could not read config at `{}`", path.display()), ) })?; - Self::from_toml(&s) + + if path.file_name().unwrap() == "pyproject.toml" { + let config = toml::from_str::(&s)?; + + if config.tool.typos.is_none() { + log::debug!("No `tool.typos` section found in `pyproject.toml`, skipping"); + + Ok(None) + } else { + Ok(config.tool.typos) + } + } else { + Self::from_toml(&s).map(Some) + } } pub fn from_toml(data: &str) -> Result { @@ -455,15 +484,14 @@ impl DictConfig { } } -fn find_project_file(dir: &std::path::Path, names: &[&str]) -> Option { - let mut file_path = dir.join("placeholder"); - for name in names { - file_path.set_file_name(name); - if file_path.exists() { - return Some(file_path); - } - } - None +fn find_project_files<'a>( + dir: &'a std::path::Path, + names: &'a [&'a str], +) -> impl Iterator + 'a { + names + .iter() + .map(|name| dir.join(name)) + .filter(|path| path.exists()) } impl PartialEq for DictConfig {