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
This commit is contained in:
Logan Hunt 2023-09-06 13:31:14 -07:00 committed by GitHub
parent 65d2fb6b91
commit b856b9603f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 56 additions and 22 deletions

View file

@ -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() { if let Some(path) = args.custom_config.as_ref() {
let custom = typos_cli::config::Config::from_file(path) let custom = typos_cli::config::Config::from_file(path)
.with_code(proc_exit::sysexits::CONFIG_ERR)?; .with_code(proc_exit::sysexits::CONFIG_ERR)?;
overrides.update(&custom); if let Some(custom) = custom {
overrides.update(&custom);
}
} }
overrides.update(&args.config.to_config()); overrides.update(&args.config.to_config());
engine.set_overrides(overrides); 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() { if let Some(path) = args.custom_config.as_ref() {
let custom = typos_cli::config::Config::from_file(path) let custom = typos_cli::config::Config::from_file(path)
.with_code(proc_exit::sysexits::CONFIG_ERR)?; .with_code(proc_exit::sysexits::CONFIG_ERR)?;
overrides.update(&custom); if let Some(custom) = custom {
overrides.update(&custom);
}
} }
overrides.update(&args.config.to_config()); overrides.update(&args.config.to_config());
engine.set_overrides(overrides); 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() { if let Some(path) = args.custom_config.as_ref() {
let custom = typos_cli::config::Config::from_file(path) let custom = typos_cli::config::Config::from_file(path)
.with_code(proc_exit::sysexits::CONFIG_ERR)?; .with_code(proc_exit::sysexits::CONFIG_ERR)?;
overrides.update(&custom); if let Some(custom) = custom {
overrides.update(&custom);
}
} }
overrides.update(&args.config.to_config()); overrides.update(&args.config.to_config());
engine.set_overrides(overrides); engine.set_overrides(overrides);

View file

@ -4,7 +4,8 @@ use kstring::KString;
use crate::file_type_specifics; 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)] #[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
@ -19,18 +20,33 @@ pub struct Config {
pub overrides: EngineConfig, 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<Config>,
}
impl Config { impl Config {
pub fn from_dir(cwd: &std::path::Path) -> Result<Option<Self>, anyhow::Error> { pub fn from_dir(cwd: &std::path::Path) -> Result<Option<Self>, anyhow::Error> {
let config = if let Some(path) = find_project_file(cwd, SUPPORTED_FILE_NAMES) { for file in find_project_files(cwd, SUPPORTED_FILE_NAMES) {
log::debug!("Loading {}", path.display()); log::debug!("Loading {}", file.display());
Some(Self::from_file(&path)?) if let Some(config) = Self::from_file(&file)? {
} else { return Ok(Some(config));
None }
}; }
Ok(config)
Ok(None)
} }
pub fn from_file(path: &std::path::Path) -> Result<Self, anyhow::Error> { pub fn from_file(path: &std::path::Path) -> Result<Option<Self>, anyhow::Error> {
let s = std::fs::read_to_string(path).map_err(|err| { let s = std::fs::read_to_string(path).map_err(|err| {
let kind = err.kind(); let kind = err.kind();
std::io::Error::new( std::io::Error::new(
@ -38,7 +54,20 @@ impl Config {
format!("could not read config at `{}`", path.display()), format!("could not read config at `{}`", path.display()),
) )
})?; })?;
Self::from_toml(&s)
if path.file_name().unwrap() == "pyproject.toml" {
let config = toml::from_str::<PyprojectTomlConfig>(&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<Self, anyhow::Error> { pub fn from_toml(data: &str) -> Result<Self, anyhow::Error> {
@ -455,15 +484,14 @@ impl DictConfig {
} }
} }
fn find_project_file(dir: &std::path::Path, names: &[&str]) -> Option<std::path::PathBuf> { fn find_project_files<'a>(
let mut file_path = dir.join("placeholder"); dir: &'a std::path::Path,
for name in names { names: &'a [&'a str],
file_path.set_file_name(name); ) -> impl Iterator<Item = std::path::PathBuf> + 'a {
if file_path.exists() { names
return Some(file_path); .iter()
} .map(|name| dir.join(name))
} .filter(|path| path.exists())
None
} }
impl PartialEq for DictConfig { impl PartialEq for DictConfig {