From d45954a4c19d9d07cd41e33ed46804fdf9cffcc2 Mon Sep 17 00:00:00 2001 From: corey <corey@nxp.com> Date: Sun, 29 Dec 2024 13:59:49 -0600 Subject: [PATCH] Continue ParamStr dev --- .../tests/utils/test_param_str.py | 423 +++++++++++++++++- rust/origen_metal/src/utils/param_str.rs | 160 ++++++- rust/pyapi/Cargo.lock | 10 + rust/pyapi_metal/src/utils/param_str.rs | 98 +++- 4 files changed, 664 insertions(+), 27 deletions(-) diff --git a/python/origen_metal/tests/utils/test_param_str.py b/python/origen_metal/tests/utils/test_param_str.py index 702de213..d2979145 100644 --- a/python/origen_metal/tests/utils/test_param_str.py +++ b/python/origen_metal/tests/utils/test_param_str.py @@ -22,6 +22,10 @@ def a4(self): def a5(self): return "arg5" + @pytest.fixture + def a6(self): + return "arg6" + @property def m(self): return "missing" @@ -30,6 +34,36 @@ def m(self): def missing(self): return self.m + @pytest.fixture + def leading(self): + return "leading" + + @pytest.fixture + def reparse_error(self): + return r"ParamStr has already been parsed. Please use 'param_str.clear\(\)', before parsing new input" + + @pytest.fixture + def leading_str_not_allowed_msg(self): + return "Attempted to set leading value but 'allows_leading_str' is not allowed" + + @pytest.fixture + def set_leading_str_after_parse_error_msg(self): + return "Attempted to change ParamStr's 'allows_leading_str' setting after parsing, which is not allowed" + + def removing_missing_def_err_msg(self, default): + return f"No parameter '{default}' to remove from ParamStr's defaults" + + def not_allowed_param_msg(self, param): + return f"ParamStr encountered parameter '{param}', which is not an expected parameter" + + @pytest.fixture + def defs_update_after_parse_err_msg(self): + return "Attempted to update ParamStr's default values after parsing, which is not allowed" + + @pytest.fixture + def update_allows_non_defaults_after_parse_err_msg(self): + return "Cannot set ParamStr's allows_non_defaults with no default parameters" + class TestStandaloneParamStr(Common): @classmethod def to_vals(cls, vals): @@ -230,32 +264,31 @@ def assert_param_str(cls, p, as_str, as_dict=None, leading=None, raw=None): assert p[k] assert p.get(k) is None - def test_leading_str(self, a1, a2): in_str = "blah" p = ParamStr(True) p.parse(in_str) - self.assert_param_str(p, "", None, leading=in_str, raw=in_str) + self.assert_param_str(p, in_str, None, leading=in_str, raw=in_str) in_str = "blah~" p = ParamStr(allows_leading_str=True) p.parse(in_str) - self.assert_param_str(p, "", None, "blah", in_str) + self.assert_param_str(p, "blah", None, "blah", in_str) in_str = "blah~arg1" p = ParamStr(allows_leading_str=True) p.parse(in_str) - self.assert_param_str(p, a1, {a1: []}, "blah", in_str) + self.assert_param_str(p, in_str, {a1: []}, "blah", in_str) in_str = f"blah blah~{a1}:v1:v2~{a2}:w2" p = ParamStr(allows_leading_str=True) p.parse(in_str) - self.assert_param_str(p, f"{a1}:v1:v2~{a2}:w2", {a1: ["v1", "v2"], a2: ["w2"]}, "blah blah", in_str) + self.assert_param_str(p, in_str, {a1: ["v1", "v2"], a2: ["w2"]}, "blah blah", in_str) in_str = "~blah" p = ParamStr(allows_leading_str=True) p.parse(in_str) - self.assert_param_str(p, "blah", {"blah": []}, "", in_str) + self.assert_param_str(p, "~blah", {"blah": []}, "", in_str) def test_key_order(self, a3, a2, a1): in_str = f"{a3}~{a2}:val~{a1}:v1:v2" @@ -354,26 +387,27 @@ def test_default_keys(self, a1, a2): assert str(p) == f"{a2}:default" # Try with some values + p = ParamStr(allows_leading_str=False, defaults=defs) in_str = f"{a1}~{a2}:not:default" p.parse(in_str) assert p.parsed == {a1: [], a2: ['not', 'default']} assert str(p) == f"{a1}~{a2}:not:default" - p.parse(in_str) def test_defaults_with_array(self, a1, a2, a3, a4): defs = {a1: ['default_1', 'default_2'], a2: ['default'], a3: [], a4: None} p = ParamStr(allows_leading_str=False, defaults=defs) - p = ParamStr(allows_leading_str=False, defaults=defs) assert p.defaults == defs assert p.parsed is None assert p.allows_non_defaults is False in_str = "" + p = ParamStr(allows_leading_str=False, defaults=defs) p.parse(in_str) assert p.parsed == {a1: ['default_1', 'default_2'], a2: ['default'], a3: []} assert str(p) == f"{a1}:default_1:default_2~{a2}:default~{a3}" in_str = f"{a2}~{a3}:a:b:c:d" + p = ParamStr(allows_leading_str=False, defaults=defs) p.parse(in_str) assert p.parsed == {a1: ['default_1', 'default_2'], a2: [], a3: ['a', 'b', 'c', 'd']} assert str(p) == f"{a2}~{a3}:a:b:c:d~{a1}:default_1:default_2" @@ -413,10 +447,383 @@ def test_allowing_non_defaults(self, a1, a2, missing): assert p.parsed == {a1: ['default'], a2: ['val'], missing: []} assert str(p) == f"{a2}:val~{missing}~{a1}:default" + p = ParamStr(allows_leading_str=False, defaults=defs, allows_non_defaults=True) p.parse(f"{a2}:val~{missing}:m:val") assert p.parsed == {a1: ['default'], a2: ['val'], missing: ['m', 'val']} assert str(p) == f"{a2}:val~{missing}:m:val~{a1}:default" + def test_try_parse(self, a1, a2, a3): + p = ParamStr() + err = p.try_parse("~") + self.assert_unparsed(p) + assert isinstance(err, RuntimeError) + assert str(err) == "ParamStr encountered a parameter with an empty key, which is not allowed" + + p = ParamStr(defaults={a1: None, a2: []}) + err = p.try_parse(f"{a3}:fail") + self.assert_unparsed(p) + assert isinstance(err, RuntimeError) + assert str(err) == self.not_allowed_param_msg(a3) + + p = ParamStr(defaults={a1: None, a2: []}) + parsed = p.try_parse(f"{a1}:should:pass") + assert isinstance(parsed, ParamStr) + assert p.parsed == {a1: ['should', 'pass'], a2: []} + assert str(p) == f"{a1}:should:pass~{a2}" + + def test_clear(self, a1, a2): + defs = {a1: ['default'], a2: []} + p = ParamStr(allows_leading_str=True, defaults=defs, allows_non_defaults=True) + self.assert_unparsed(p) + assert p.defaults == defs + assert p.allows_leading_str == True + assert p.allows_non_defaults == True + + # No change to setup upon clear + assert isinstance(p.clear(), ParamStr) + self.assert_unparsed(p) + assert p.defaults == defs + assert p.allows_leading_str == True + assert p.allows_non_defaults == True + + leading = "leading" + in_str = f"{leading}~{a2}:not:default" + p.parse(in_str) + assert p.parsed == {a1: ['default'], a2: ['not', 'default']} + assert p.raw == in_str + assert p.leading == leading + + assert isinstance(p.clear(), ParamStr) + self.assert_unparsed(p) + assert p.defaults == defs + assert p.allows_leading_str == True + assert p.allows_non_defaults == True + + def test_duplicating_unparsed_instance(self, a1, a2): + defs = {a1: ['default'], a2: []} + leading = "leading" + p = ParamStr(allows_leading_str=True, defaults=defs) + self.assert_unparsed(p) + assert p.defaults == defs + assert p.allows_leading_str == True + assert p.allows_non_defaults == False + + p2 = p.duplicate() + self.assert_unparsed(p2) + assert p2.defaults == defs + assert p2.allows_leading_str == True + assert p2.allows_non_defaults == False + in_str_p2 = f"{leading}~{a1}~{a2}:val" + p2.parse(in_str_p2) + assert p2.parsed == {a1: [], a2: ['val']} + assert str(p2) == in_str_p2 + assert p2.raw == in_str_p2 + assert p2.leading == leading + + p3 = p.dup() + self.assert_unparsed(p3) + assert p3.defaults == defs + assert p3.allows_leading_str == True + assert p3.allows_non_defaults == False + + in_str = f"p3~{a1}:new" + p3.parse(in_str) + assert p3.parsed == {a1: ['new'], a2: []} + assert str(p3) == f"{in_str}~{a2}" + assert p3.raw == in_str + assert p3.leading == "p3" + + # p and p2 should remain unchanged + self.assert_unparsed(p) + assert p2.parsed == {a1: [], a2: ['val']} + assert str(p2) == f"{leading}~{a1}~{a2}:val" + assert p2.raw == in_str_p2 + assert p2.leading == leading + + def test_duplicating_parsed_instance(self, a1, a2, a3): + defs = {a1: [], a2: []} + p = ParamStr.and_parse(f"{a3}:v1", defaults=defs, allows_non_defaults=True) + assert p.parsed == {a1: [], a2: [], a3: ['v1']} + + p2 = p.duplicate() + assert p2.parsed == {a1: [], a2: [], a3: ['v1']} + + p3 = p2.dup() + assert p3.parsed == {a1: [], a2: [], a3: ['v1']} + + p.clear() + self.assert_unparsed(p) + assert p2.parsed == {a1: [], a2: [], a3: ['v1']} + assert p3.parsed == {a1: [], a2: [], a3: ['v1']} + + p2.clear() + self.assert_unparsed(p2) + assert p3.parsed == {a1: [], a2: [], a3: ['v1']} + + def test_error_on_reparse(self, a1, a2, a3, reparse_error): + parsed = {a1: [], a2: ['val']} + leading = 'leading' + in_str = f"{leading}~{a1}~{a2}:val" + p = ParamStr(allows_leading_str=True) + p.parse(in_str) + assert p.parsed == parsed + assert p.raw == in_str + assert p.leading == leading + + with pytest.raises(RuntimeError, match=reparse_error): + p.parse(f"{a1}~{a3}") + assert p.parsed == parsed + assert p.raw == in_str + assert p.leading == leading + + def test_setting_parameters(self, a1, a2, a3, reparse_error): + p = ParamStr() + assert p.set(a1, 'val') == False + assert p.parsed == {a1: ['val']} + assert p.raw == None + assert p.leading == None + + assert p.set(a2, []) == False + assert p.set(a3, ['v', '3']) == False + assert p.parsed == {a1: ['val'], a2: [], a3: ['v', '3']} + assert p.raw == None + assert p.leading == None + + assert p.set(a2, None) == True + assert p.parsed == {a1: ['val'], a3: ['v', '3']} + + with pytest.raises(RuntimeError, match=reparse_error): + p.parse("") + + def test_setting_parameters_after_parse(self, a1, a2, a3): + p = ParamStr() + in_str = f"{a1}:v1~{a2}" + p.parse(in_str) + assert p.parsed == {a1: ['v1'], a2: []} + assert p.leading == None + assert p.raw == in_str + + assert p.set(a3, []) == False + assert p.parsed == {a1: ['v1'], a2: [], a3: []} + assert p.raw == in_str + + assert p.set(a2, ['v', '2']) == True + assert p.set(a1, None) == True + assert p.parsed == {a2: ['v', '2'], a3: []} + assert p.raw == in_str + + def test_setting_parameters_with_defaults(self, a1, a2, a3, a4): + defs = {a1: 'v1', a2: [], a3: ['v', '3']} + p = ParamStr(defaults=defs) + assert p.set(a1, 'val') == True + assert p.parsed == {a1: ['val'], a2: [], a3: ['v', '3']} + + p = ParamStr(defaults=defs) + with pytest.raises(RuntimeError, match=self.not_allowed_param_msg(a4)): + p.set(a4, None) + # This will still initialize the ParamStr + assert p.parsed == {a1: ['v1'], a2: [], a3: ['v', '3']} + + p = ParamStr(defaults=defs, allows_non_defaults=True) + assert p.set(a2, None) == True + assert p.set(a4, []) == False + assert p.parsed == {a1: ['v1'], a3: ['v', '3'], a4: []} + + @pytest.mark.skip + def test_setting_with_dict(self): + fail + + @pytest.mark.skip + def test_duplicate_param(self): + fail + + def test_setting_leading_str(self, a1): + p = ParamStr(allows_leading_str=True) + + lead_str = "test_setting" + assert p.set_leading(lead_str) == False + assert p.leading == lead_str + assert str(p) == lead_str + + lead_str2 = "test setting 2" + assert p.set_leading(lead_str2) == True + assert p.leading == lead_str2 + p.set(a1, "val") + assert str(p) == f"{lead_str2}~{a1}:val" + + def test_setting_leading_str_after_parse(self, a1, a2): + p = ParamStr(allows_leading_str=True) + in_str = f"~{a1}~{a2}:v" + p.parse(in_str) + assert p.leading == '' + assert p.raw == in_str + + leading = "leading" + assert p.set_leading(leading) == True + assert p.leading == leading + assert p.raw == in_str + assert str(p) == f"{leading}{in_str}" + + def test_error_setting_leading_str_if_not_allowed(self, leading_str_not_allowed_msg): + p = ParamStr() + with pytest.raises(RuntimeError, match=leading_str_not_allowed_msg): + p.set_leading("leading") + + p.parse("") + assert p.parsed == {} + assert p.leading == None + with pytest.raises(RuntimeError, match=leading_str_not_allowed_msg): + p.set_leading("leading") + + def test_setting_allows_leading_str(self, a1, leading, leading_str_not_allowed_msg): + p = ParamStr() + assert p.allows_leading_str == False + + assert p.set_allows_leading_str(True) == None + assert p.allows_leading_str == True + self.assert_unparsed(p) + p.set_leading(leading) == True + + p.clear() + assert p.allows_leading_str == True + p.parse(f"{leading}~{a1}") + assert p.leading == leading + assert p.parsed == {a1: []} + + p.clear() + assert p.set_allows_leading_str(False) == None + assert p.allows_leading_str == False + with pytest.raises(RuntimeError, match=leading_str_not_allowed_msg): + p.set_leading("leading") + + def test_error_setting_allows_leading_str_after_parse(self, leading_str_not_allowed_msg, set_leading_str_after_parse_error_msg): + p = ParamStr() + assert p.allows_leading_str == False + p.parse("") + + with pytest.raises(RuntimeError, match=set_leading_str_after_parse_error_msg): + p.set_allows_leading_str(True) + + # Allows leading should still be False + assert p.allows_leading_str == False + with pytest.raises(RuntimeError, match=leading_str_not_allowed_msg): + p.set_leading("leading") + + with pytest.raises(RuntimeError, match=set_leading_str_after_parse_error_msg): + p.set_allows_leading_str(False) + assert p.allows_leading_str == False + + def test_setting_defaults(self, a1, a2, a3, a4, a5, a6): + p = ParamStr() + assert p.defaults == None + assert p.allows_non_defaults == None + + assert p.add_default(a1, None) == False + assert p.add_default(a2, 'v1') == False + assert p.add_default(a3, ['v', '2']) == False + assert p.allows_non_defaults == False + assert p.defaults == {a1: None, a2: ['v1'], a3: ['v', '2']} + p.parse("") + assert p.parsed == {a2: ['v1'], a3: ['v', '2']} + + defs = {a1: ['v1'], a2: None} + p = ParamStr(defaults=defs) + assert p.defaults == defs + assert p.add_default(a3, None) == False + assert p.add_default(a1, None) == True + assert p.defaults == {a1: None, a2: None, a3: None} + + assert p.add_defaults({a4: None, a5: ['v' , '5']}) == [False, False] + assert p.defaults == {a1: None, a2: None, a3: None, a4: None, a5: ['v', '5']} + + assert p.add_defaults({a2: None, a3: ['v3'], a6: 'v6'}) == [True, True, False] + assert p.defaults == {a1: None, a2: None, a3: ['v3'], a4: None, a5: ['v', '5'], a6: ['v6']} + + def test_removing_defaults(self, a1, a2, a3, a4, a5): + defs = {a1: None, a2: 'val', a3: ['v', '3'], a4: [], a5: 'v5'} + defs_added = {a1: None, a2: ['val'], a3: ['v', '3'], a4: [], a5: ['v5']} + p = ParamStr(defaults=defs) + assert p.defaults == defs_added + + assert p.remove_default(a1) == None + defs_added.pop(a1) + assert p.defaults == defs_added + + assert p.remove_default(a2) == ['val'] + assert p.remove_default(a3) == ['v', '3'] + assert p.remove_default(a4) == [] + defs_added.pop(a2) + defs_added.pop(a3) + defs_added.pop(a4) + assert p.defaults == defs_added + p.parse("") + assert str(p) == f"{a5}:v5" + + p = ParamStr(defaults=defs) + assert p.remove_defaults([a1, a3, a4, a5]) == [None, ['v', '3'], [], ['v5']] + assert p.defaults == {a2: ['val']} + + with pytest.raises(RuntimeError, match=self.not_allowed_param_msg(a1)): + p.parse(f'{a1}:try') + + p.parse(f"{a2}:override") + assert str(p) == f"{a2}:override" + + def test_error_removing_missing_default(self, a1, a2, a3): + defs = {a1: ['v1'], a2: []} + p = ParamStr(defaults=defs) + with pytest.raises(RuntimeError, match=self.removing_missing_def_err_msg(a3)): + p.remove_default(a3) + with pytest.raises(RuntimeError, match=self.removing_missing_def_err_msg(a3)): + p.remove_defaults([a1, a3]) + assert p.defaults == defs + + def test_error_changing_defaults_after_parse(self, a1, a2, a3, a4, defs_update_after_parse_err_msg): + defs = {a1: [], a2: None} + p = ParamStr(defaults=defs) + p.parse("") + with pytest.raises(RuntimeError, match=defs_update_after_parse_err_msg): + p.add_default(a3, []) + assert p.defaults == defs + + with pytest.raises(RuntimeError, match=defs_update_after_parse_err_msg): + p.add_defaults({a3: 'val', a4: 'val'}) + assert p.defaults == defs + + with pytest.raises(RuntimeError, match=defs_update_after_parse_err_msg): + p.add_defaults({a1: 'v', a3: 'v3'}) + assert p.defaults == defs + + with pytest.raises(RuntimeError, match=defs_update_after_parse_err_msg): + p.remove_default(a3) + assert p.defaults == defs + + with pytest.raises(RuntimeError, match=defs_update_after_parse_err_msg): + p.remove_defaults([a3, a4]) + assert p.defaults == defs + + with pytest.raises(RuntimeError, match=defs_update_after_parse_err_msg): + p.remove_defaults([a1, a4]) + assert p.defaults == defs + + def test_setting_allows_non_defaults(self, a1, a2, a3, update_allows_non_defaults_after_parse_err_msg): + p = ParamStr(defaults={a1: [], a2: None}) + assert p.allows_non_defaults == False + assert p.set_allows_non_defaults(True) == None + assert p.allows_non_defaults == True + p.parse(a3) + assert p.parsed == {a1: [], a3: []} + + with pytest.raises(RuntimeError, match=update_allows_non_defaults_after_parse_err_msg): + p.set_allows_non_defaults(False) + assert p.allows_non_defaults == True + + p = ParamStr(defaults={a1: [], a2: None}, allows_non_defaults=True) + assert p.allows_non_defaults == True + assert p.set_allows_non_defaults(False) == None + with pytest.raises(RuntimeError, match=self.not_allowed_param_msg(a3)): + p.parse(a3) + class TestMultiParamStr(Common): @classmethod def assert_pre_parsed(cls, mps): diff --git a/rust/origen_metal/src/utils/param_str.rs b/rust/origen_metal/src/utils/param_str.rs index e35a9f7a..c0507614 100644 --- a/rust/origen_metal/src/utils/param_str.rs +++ b/rust/origen_metal/src/utils/param_str.rs @@ -29,7 +29,30 @@ impl ParamStr { } } + pub fn init_parsed(&mut self, allow_parsed: bool) -> Result<bool> { + if self.parsed.is_some() { + if allow_parsed { + return Ok(false); + } else { + bail!("ParamStr has already been parsed. Please use 'param_str.clear()', before parsing new input"); + } + } + let mut parsed = IndexMap::new(); + if let Some(defs) = self.defaults.as_ref() { + for (name, def_val) in &defs.1 { + if let Some(v) = def_val { + parsed.insert(name.to_owned(), v.to_owned()); + } + } + } + self.parsed = Some(parsed); + Ok(true) + } + pub fn parse(&mut self, input: String) -> Result<&Self> { + if self.parsed.is_some() { + bail!("ParamStr has already been parsed. Please use 'param_str.clear()', before parsing new input"); + } let input_inter; let leading; let mut parsed = IndexMap::new(); @@ -139,6 +162,35 @@ impl ParamStr { Ok(self) } + fn param_set(&mut self, param: String, val: Option<Vec<String>>) -> Result<bool> { + match self.parsed.as_mut() { + Some(params) => { + let retn = params.contains_key(¶m); + if let Some(defs) = self.defaults.as_ref() { + if (!defs.0) && (!defs.1.contains_key(¶m)) { + bail!("ParamStr encountered parameter '{}', which is not an expected parameter", ¶m); + } + } + if let Some(v) = val { + params.insert(param, v); + } else { + params.remove(¶m); + } + Ok(retn) + }, + None => bail!(*NOT_PARSED_MSG) + } + } + + pub fn set_param(&mut self, param: String, val: Option<Vec<String>>) -> Result<bool> { + if self.parsed.is_some() { + self.param_set(param, val) + } else { + self.init_parsed(true)?; + self.param_set(param, val) + } + } + pub fn defaults(&self) -> Option<&IndexMap<String, Option<Vec<String>>>> { if let Some(defs) = self.defaults.as_ref() { Some(&defs.1) @@ -147,6 +199,64 @@ impl ParamStr { } } + pub fn with_mut_defs<T, F>(&mut self, f: F) -> Result<T> + where + F: FnOnce(&mut (bool, IndexMap<String, Option<Vec<String>>>)) -> Result<T>, + { + if self.parsed.is_some() { + bail!("Attempted to update ParamStr's default values after parsing, which is not allowed") + } + if self.defaults.is_none() { + self.defaults = Some((false, IndexMap::new())); + } + f(self.defaults.as_mut().unwrap()) + } + + pub fn add_default(&mut self, def: String, value: Option<Vec<String>>) -> Result<bool> { + self.with_mut_defs( |defs| { + let retn = defs.1.contains_key(&def); + defs.1.insert(def, value); + Ok(retn) + }) + } + + pub fn add_defaults(&mut self, to_add: IndexMap<String, Option<Vec<String>>>) -> Result<Vec<bool>> { + self.with_mut_defs( |defs| { + let mut retn = vec!(); + for (name, val) in to_add { + retn.push(defs.1.contains_key(&name)); + defs.1.insert(name, val); + } + Ok(retn) + }) + } + + pub fn remove_default(&mut self, to_remove: &str) -> Result<Option<Vec<String>>> { + self.with_mut_defs( |defs| { + match defs.1.shift_remove(to_remove) { + Some(value) => Ok(value), + None => bail!("No parameter '{}' to remove from ParamStr's defaults", to_remove) + } + }) + } + + pub fn remove_defaults(&mut self, to_remove: &Vec<String>) -> Result<Vec<Option<Vec<String>>>> { + self.with_mut_defs( |defs| { + // Check that all keys are valid first + for name in to_remove { + if !defs.1.contains_key(name) { + bail!("No parameter '{}' to remove from ParamStr's defaults", name) + } + } + + let mut retn = vec!(); + for name in to_remove { + retn.push(defs.1.shift_remove(name).unwrap()); + } + Ok(retn) + }) + } + pub fn allows_non_defaults(&self) -> Option<bool> { if let Some(setup) = self.defaults.as_ref() { Some(setup.0) @@ -155,6 +265,16 @@ impl ParamStr { } } + pub fn set_allows_non_defaults(&mut self, new_val: bool) -> Result<()> { + if self.parsed.is_some() { + bail!("Cannot set ParamStr's allows_non_defaults with no default parameters"); + } + self.with_mut_defs( |defs| { + defs.0 = new_val; + Ok(()) + }) + } + pub fn parsed(&self) -> &Option<IndexMap<String, Vec<String>>> { &self.parsed } @@ -167,6 +287,13 @@ impl ParamStr { } } + pub fn clear(&mut self) -> Result<&mut Self> { + self.parsed = None; + self.leading = None; + self.raw = None; + Ok(self) + } + pub fn raw(&self) -> Result<&Option<String>> { if self.parsed.is_some() { Ok(&self.raw) @@ -183,10 +310,28 @@ impl ParamStr { } } + pub fn set_leading(&mut self, new_leading: Option<String>) -> Result<bool> { + if !self.allows_leading_str { + bail!("Attempted to set leading value but 'allows_leading_str' is not allowed") + } + self.init_parsed(true)?; + let retn = self.leading.is_some(); + self.leading = new_leading; + Ok(retn) + } + pub fn allows_leading_str(&self) -> bool { self.allows_leading_str } - + + pub fn set_allows_leading_str(&mut self, new_val: bool) -> Result<()> { + if self.parsed.is_some() { + bail!("Attempted to change ParamStr's 'allows_leading_str' setting after parsing, which is not allowed"); + } + self.allows_leading_str = new_val; + Ok(()) + } + pub fn get(&self, key: &str) -> Result<Option<&Vec<String>>> { if let Some(args) = self.parsed.as_ref() { Ok(args.get(key)) @@ -197,13 +342,22 @@ impl ParamStr { pub fn to_string(&self) -> Result<String> { if let Some(args) = self.parsed.as_ref() { - Ok(args.iter().map(|(k, v)| { + let params = args.iter().map(|(k, v)| { if v.is_empty() { k.clone() } else { format!("{}:{}", k, v.join(":")) } - }).collect::<Vec<String>>().join("~")) + }).collect::<Vec<String>>().join("~"); + if let Some(leading) = self.leading.as_ref() { + if params.is_empty() { + Ok(leading.clone()) + } else { + Ok(format!("{}~{}", leading, params)) + } + } else { + Ok(params) + } } else { bail!(*NOT_PARSED_MSG); } diff --git a/rust/pyapi/Cargo.lock b/rust/pyapi/Cargo.lock index ab5b2f6b..6410b497 100644 --- a/rust/pyapi/Cargo.lock +++ b/rust/pyapi/Cargo.lock @@ -2132,6 +2132,15 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minijinja" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c37e1b517d1dcd0e51dc36c4567b9d5a29262b3ec8da6cb5d35e27a8fb529b5" +dependencies = [ + "serde", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2636,6 +2645,7 @@ dependencies = [ "ldap3", "lettre", "md-5", + "minijinja", "num-bigint 0.4.5", "num-traits", "octocrab", diff --git a/rust/pyapi_metal/src/utils/param_str.rs b/rust/pyapi_metal/src/utils/param_str.rs index d2a2dda6..6f9e10fc 100644 --- a/rust/pyapi_metal/src/utils/param_str.rs +++ b/rust/pyapi_metal/src/utils/param_str.rs @@ -62,6 +62,10 @@ impl ParamStr { } } + pub fn set_allows_non_defaults(&mut self, new_allows_non_defaults: bool) -> PyResult<()> { + Ok(self.om.set_allows_non_defaults(new_allows_non_defaults)?) + } + #[getter] pub fn defaults<'py>(&self, py: Python<'py>) -> PyResult<Option<&'py PyDict>> { if let Some(defs) = self.om.defaults() { @@ -75,11 +79,41 @@ impl ParamStr { } } + pub fn add_default(&mut self, name: String, value: &PyAny) -> PyResult<bool> { + Ok(self.om.add_default(name, Self::extract_param_value(value)?)?) + } + + pub fn add_defaults(&mut self, to_add: &PyDict) -> PyResult<Vec<bool>> { + Ok(self.om.add_defaults(Self::extract_defaults(to_add)?)?) + } + + pub fn remove_default(&mut self, to_remove: &str) -> PyResult<Option<Vec<String>>> { + Ok(self.om.remove_default(to_remove)?) + } + + pub fn remove_defaults(&mut self, to_remove: Vec<String>) -> PyResult<Vec<Option<Vec<String>>>> { + Ok(self.om.remove_defaults(&to_remove)?) + } + pub fn parse(mut slf: PyRefMut<Self>, input: String) -> PyResult<PyRefMut<Self>> { slf.om.parse(input)?; Ok(slf) } + /// If the ParamStr fails to parse, returns the exception instead of raising one. + /// Non-ParamStr parse exceptions will still be raised (such is missing the input argument) + pub fn try_parse<'py>(mut slf: PyRefMut<Self>, py: Python<'py>, input: String) -> PyResult<PyObject> { + Ok(match slf.om.parse(input) { + Ok(_) => slf.into_py(py), + Err(e) => runtime_exception!(e.msg).to_object(py) + }) + } + + pub fn clear(mut slf: PyRefMut<Self>) -> PyResult<PyRefMut<Self>> { + slf.om.clear()?; + Ok(slf) + } + #[getter] pub fn raw(&self) -> PyResult<Option<String>> { Ok(self.om.raw()?.to_owned()) @@ -90,11 +124,19 @@ impl ParamStr { Ok(self.om.leading()?.to_owned()) } + pub fn set_leading(&mut self, new_leading: Option<String>) -> PyResult<bool> { + Ok(self.om.set_leading(new_leading)?) + } + #[getter] pub fn allows_leading_str(&self) -> PyResult<bool> { Ok(self.om.allows_leading_str()) } + pub fn set_allows_leading_str(&mut self, new_leading: bool) -> PyResult<()> { + Ok(self.om.set_allows_leading_str(new_leading)?) + } + fn to_str(&self) -> PyResult<String> { Ok(self.om.to_string()?) } @@ -137,7 +179,7 @@ impl ParamStr { fn __iter__(slf: PyRefMut<Self>) -> PyResult<ParamStrIter> { Ok(ParamStrIter { - keys: slf.keys().unwrap(), + keys: slf.keys()?, i: 0, }) } @@ -154,6 +196,24 @@ impl ParamStr { _ => Ok(py.NotImplemented()), }) } + + fn dup(&self) -> PyResult<Self> { + Ok(Self { + om: self.om.clone() + }) + } + + fn duplicate(&self) -> PyResult<Self> { + self.dup() + } + + fn set_param(&mut self, param: String, value: &PyAny) -> PyResult<bool> { + Ok(self.om.set_param(param, Self::extract_param_value(value)?)?) + } + + fn set(&mut self, param: String, value: &PyAny) -> PyResult<bool> { + self.set_param(param, value) + } } impl ParamStr { @@ -166,26 +226,32 @@ impl ParamStr { fn new_om(allows_leading_str: bool, defaults: Option<&PyDict>, allows_non_defaults: Option<bool>) -> Result<OmParamStr> { let om_defaults; if let Some(defs) = defaults { - let mut om_defs = IndexMap::new(); - for (key, default) in defs { - let def; - if let Ok(s) = default.extract::<String>() { - def = Some(vec!(s)); - } else if let Ok(v) = default.extract::<Vec<String>>() { - def = Some(v); - } else if default.is_none() { - def = None - } else { - bail!("ParamStr default value must be either None, a str, or list of strs"); - } - om_defs.insert(key.extract::<String>()?, def); - } - om_defaults = Some((allows_non_defaults.unwrap_or(false), om_defs)) + om_defaults = Some((allows_non_defaults.unwrap_or(false), Self::extract_defaults(defs)?)); } else { om_defaults = None } Ok(OmParamStr::new(allows_leading_str, om_defaults)) } + + fn extract_defaults(defs: &PyDict) -> PyResult<IndexMap<String, Option<Vec<String>>>> { + let mut om_defs = IndexMap::new(); + for (key, default) in defs { + om_defs.insert(key.extract::<String>()?, Self::extract_param_value(default)?); + } + Ok(om_defs) + } + + fn extract_param_value(val: &PyAny) -> Result<Option<Vec<String>>> { + Ok(if let Ok(s) = val.extract::<String>() { + Some(vec!(s)) + } else if let Ok(v) = val.extract::<Vec<String>>() { + Some(v) + } else if val.is_none() { + None + } else { + bail!("ParamStr value must be either None, a str, or list of strs"); + }) + } } #[pyclass]