Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support @fromfile option values in the Rust options parser. #20634

Merged
merged 6 commits into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/rust/engine/options/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::env;

use super::id::{NameTransform, OptionId, Scope};
use super::{DictEdit, OptionsSource};
use crate::parse::{expand, expand_to_dict, expand_to_list, ListMember, Parseable};
use crate::parse::{expand, expand_to_dict, expand_to_list, Parseable};
use crate::ListEdit;
use std::collections::HashMap;

Expand Down Expand Up @@ -77,7 +77,7 @@ impl Args {
Ok(None)
}

fn get_list<T: ListMember>(&self, id: &OptionId) -> Result<Option<Vec<ListEdit<T>>>, String> {
fn get_list<T: Parseable>(&self, id: &OptionId) -> Result<Option<Vec<ListEdit<T>>>, String> {
let arg_names = Self::arg_names(id, Negate::False);
let mut edits = vec![];
for arg in &self.args {
Expand Down
10 changes: 10 additions & 0 deletions src/rust/engine/options/src/args_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,16 @@ fn test_scalar_fromfile() {
do_test("-42", -42, Args::get_int, false);
do_test("3.14", 3.14, Args::get_float, false);
do_test("EXPANDED", "EXPANDED".to_owned(), Args::get_string, false);

let (_tmpdir, fromfile_path) = write_fromfile("fromfile.txt", "BAD INT");
let args = Args {
args: vec![format!("--foo=@{}", fromfile_path.display())],
};
assert_eq!(
args.get_int(&option_id!("foo")).unwrap_err(),
"Problem parsing --foo int value:\n1:BAD INT\n ^\n\
Expected \"+\", \"-\" or ['0'..='9'] at line 1 column 1"
benjyw marked this conversation as resolved.
Show resolved Hide resolved
);
}

#[test]
Expand Down
4 changes: 2 additions & 2 deletions src/rust/engine/options/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use toml::value::Table;
use toml::Value;

use super::id::{NameTransform, OptionId};
use super::parse::{expand, expand_to_dict, expand_to_list, ListMember, Parseable};
use super::parse::{expand, expand_to_dict, expand_to_list, Parseable};
use super::{DictEdit, DictEditAction, ListEdit, ListEditAction, OptionsSource, Val};

type InterpolationMap = HashMap<String, String>;
Expand Down Expand Up @@ -325,7 +325,7 @@ impl Config {
.and_then(|table| table.get(Self::option_name(id)))
}

fn get_list<T: FromValue + ListMember>(
fn get_list<T: FromValue + Parseable>(
&self,
id: &OptionId,
) -> Result<Option<Vec<ListEdit<T>>>, String> {
Expand Down
4 changes: 2 additions & 2 deletions src/rust/engine/options/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use std::ffi::OsString;

use super::id::{NameTransform, OptionId, Scope};
use super::{DictEdit, OptionsSource};
use crate::parse::{expand, expand_to_dict, expand_to_list, ListMember, Parseable};
use crate::parse::{expand, expand_to_dict, expand_to_list, Parseable};
use crate::ListEdit;

#[derive(Debug)]
Expand Down Expand Up @@ -67,7 +67,7 @@ impl Env {
names
}

fn get_list<T: ListMember>(&self, id: &OptionId) -> Result<Option<Vec<ListEdit<T>>>, String> {
fn get_list<T: Parseable>(&self, id: &OptionId) -> Result<Option<Vec<ListEdit<T>>>, String> {
for env_var_name in &Self::env_var_names(id) {
if let Some(value) = self.env.get(env_var_name) {
return expand_to_list::<T>(value.to_owned())
Expand Down
125 changes: 67 additions & 58 deletions src/rust/engine/options/src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
use super::{DictEdit, DictEditAction, ListEdit, ListEditAction, Val};
use crate::render_choice;

use serde::de::DeserializeOwned;
use log::warn;
use serde::de::{Deserialize, DeserializeOwned};
use std::collections::HashMap;
use std::fmt::Display;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -282,7 +283,7 @@ pub(crate) fn parse_dict(value: &str) -> Result<DictEdit, ParseError> {
option_value_parser::dict_edit(value).map_err(|e| format_parse_error("dict", value, e))
}

pub(crate) trait Parseable: Sized {
pub(crate) trait Parseable: Sized + DeserializeOwned {
fn parse(value: &str) -> Result<Self, ParseError>;
fn parse_list(value: &str) -> Result<Vec<ListEdit<Self>>, ParseError>;
}
Expand Down Expand Up @@ -331,28 +332,22 @@ impl Parseable for String {
}
}

pub(crate) trait ListMember: Parseable + DeserializeOwned {}

impl ListMember for bool {}

impl ListMember for i64 {}

impl ListMember for f64 {}

impl ListMember for String {}

// If the corresponding unexpanded value points to a @fromfile, then the
// first component is the path to that file, and the second is the value from the file,
// or None if the file doesn't exist and the @?fromfile syntax was used.
//
// Otherwise, the first component is None and the second is the original value.
type ExpandedValue = (Option<PathBuf>, Option<String>);

// fn mk_parse_err(err: impl Display, path_opt: Option<Path>) -> ParseError {
// let error_src = if let Some(path) = path_opt { format!("reading {path}") } else { "parsing value" };
// ParseError::new(format!("Problem {error_src} for {{name}}: {err}"))
// }

benjyw marked this conversation as resolved.
Show resolved Hide resolved
fn mk_parse_err(err: impl Display, path: &Path) -> ParseError {
ParseError::new(format!(
"Problem reading {path} for {{name}}: {err_msg}",
path = path.display(),
err_msg = err,
"Problem reading {path} for {{name}}: {err}",
path = path.display()
))
}

Expand All @@ -368,7 +363,10 @@ fn maybe_expand(value: String) -> Result<ExpandedValue, ParseError> {
let path = PathBuf::from(subsuffix);
match fs::read_to_string(&path) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I note that this is doing synchronous IO, but generally pants does async. Is the sync IO acceptable here?

Copy link
Sponsor Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably? We expect very few fromfile options, and they are read synchronously today in the Python code.

The trickier part is invalidating options when the fromfile changes (which we don't to today either). Making this async though would be a pretty big change that would be best done as its own project. For now we're no worse off than today.

Ok(content) => Ok((Some(path), Some(content))),
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok((Some(path), None)),
Err(err) if err.kind() == io::ErrorKind::NotFound => {
warn!("Optional file config '{}' does not exist.", path.display());
Ok((Some(path), None))
}
Err(err) => Err(mk_parse_err(err, &path)),
}
}
Expand All @@ -389,61 +387,72 @@ pub(crate) fn expand(value: String) -> Result<Option<String>, ParseError> {
Ok(expanded_value)
}

pub(crate) fn expand_to_list<T: ListMember>(
value: String,
) -> Result<Option<Vec<ListEdit<T>>>, ParseError> {
let (path_opt, value_opt) = maybe_expand(value)?;
if value_opt.is_none() {
return Ok(None);
}
let value = value_opt.unwrap();
let mut deserialized_items: Option<Vec<T>> = None;
if let Some(path) = path_opt {
#[derive(Debug)]
enum FromfileType {
Json,
Yaml,
Unknown,
}

impl FromfileType {
fn detect(path: &Path) -> FromfileType {
if let Some(ext) = path.extension() {
if ext == "json" {
deserialized_items =
Some(serde_json::from_str(&value).map_err(|e| mk_parse_err(e, &path))?)
return FromfileType::Json;
} else if ext == "yml" || ext == "yaml" {
deserialized_items =
Some(serde_yaml::from_str(&value).map_err(|e| mk_parse_err(e, &path))?)
}
return FromfileType::Yaml;
};
}
FromfileType::Unknown
}
if let Some(items) = deserialized_items {
Ok(Some(vec![ListEdit {
action: ListEditAction::Replace,
items,
}]))
}

fn try_deserialize<'a, DE: Deserialize<'a>>(
value: &'a str,
path_opt: Option<PathBuf>,
) -> Result<Option<DE>, ParseError> {
if let Some(path) = path_opt {
match FromfileType::detect(&path) {
FromfileType::Json => serde_json::from_str(value).map_err(|e| mk_parse_err(e, &path)),
FromfileType::Yaml => serde_yaml::from_str(value).map_err(|e| mk_parse_err(e, &path)),
_ => Ok(None),
}
} else {
T::parse_list(&value).map(Some)
Ok(None)
}
}

pub(crate) fn expand_to_dict(value: String) -> Result<Option<DictEdit>, ParseError> {
pub(crate) fn expand_to_list<T: Parseable>(
value: String,
) -> Result<Option<Vec<ListEdit<T>>>, ParseError> {
let (path_opt, value_opt) = maybe_expand(value)?;
if value_opt.is_none() {
return Ok(None);
}
let value = value_opt.unwrap();
let mut deserialized_items: Option<HashMap<String, Val>> = None;
if let Some(path) = path_opt {
if let Some(ext) = path.extension() {
if ext == "json" {
deserialized_items =
Some(serde_json::from_str(&value).map_err(|e| mk_parse_err(e, &path))?)
} else if ext == "yml" || ext == "yaml" {
deserialized_items =
Some(serde_yaml::from_str(&value).map_err(|e| mk_parse_err(e, &path))?)
}
if let Some(value) = value_opt {
if let Some(items) = try_deserialize(&value, path_opt)? {
Ok(Some(vec![ListEdit {
action: ListEditAction::Replace,
items,
}]))
} else {
T::parse_list(&value).map(Some)
}
} else {
Ok(None)
}
if let Some(items) = deserialized_items {
Ok(Some(DictEdit {
action: DictEditAction::Replace,
items,
}))
}

pub(crate) fn expand_to_dict(value: String) -> Result<Option<DictEdit>, ParseError> {
let (path_opt, value_opt) = maybe_expand(value)?;
if let Some(value) = value_opt {
if let Some(items) = try_deserialize(&value, path_opt)? {
Ok(Some(DictEdit {
action: DictEditAction::Replace,
items,
}))
} else {
parse_dict(&value).map(Some)
}
} else {
parse_dict(&value).map(Some)
Ok(None)
}
}

Expand Down
Loading
Loading