From 7479d6a8e45a837e59526610a815c9d49390cff2 Mon Sep 17 00:00:00 2001 From: Logan Drescher Date: Fri, 4 Oct 2024 08:49:18 -0400 Subject: [PATCH] Adding more implicit `ModelAttributeChange` options (with more tests) --- biosimulators_utils/sedml/utils.py | 42 ++++++++++++++++++++---------- tests/sedml/test_sedml_utils.py | 19 ++++++++++++-- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/biosimulators_utils/sedml/utils.py b/biosimulators_utils/sedml/utils.py index 1d523169..6dcc0fc9 100644 --- a/biosimulators_utils/sedml/utils.py +++ b/biosimulators_utils/sedml/utils.py @@ -541,29 +541,38 @@ def apply_changes_to_xml_model(model, model_etree, sed_doc=None, working_dir=Non xml_target_captures = regex.split(r"[\@|=]", xpath_captures[1]) xml_target_captures[2] = xml_target_captures[2][1:-1] _, target_type, target_value = tuple(xml_target_captures) - xml_model_attribute = eval_xpath(model_etree, change.target, change.target_namespaces) - if validate_unique_xml_targets and len(xml_model_attribute) != 1: + xml_model_element = eval_xpath(model_etree, change.target, change.target_namespaces) + if validate_unique_xml_targets and len(xml_model_element) != 1: raise ValueError(f'xpath {change.target} must match a single object') xpath_tiers = [elem for elem in regex.split("/", xpath_captures[0]) if ":" in elem] if len(xpath_tiers) == 0: raise ValueError('No namespace is defined') - existing_namespace = regex.split(":", xpath_tiers[0])[0] - if change.target_namespaces.get(existing_namespace) is None: - raise ValueError(f'No namespace is defined with prefix `{existing_namespace}`') + element_type = regex.split(":", xpath_tiers[-1]) + if len(element_type) != 2: + raise ValueError("Unexpected number of tokens in model element xpath") + + namespace_prefix, type_suffix = tuple(element_type) + if change.target_namespaces.get(namespace_prefix) is None: + raise ValueError(f'No namespace is defined with prefix `{namespace_prefix}`') # change value - for attribute in xml_model_attribute: - if attribute.get("initialConcentration") is not None: + for attribute in xml_model_element: + if type_suffix == "species" and attribute.get("initialConcentration") is not None: attribute.set("initialConcentration", change.new_value) + elif type_suffix == "compartment" and attribute.get("size") is not None: + attribute.set("size", change.new_value) + elif type_suffix == "parameter" and attribute.get("value") is not None: + attribute.set("value", change.new_value) else: - raise ValueError(f"SBML attribute to apply `{change.new_value}` to can not be figured out.") + change.model = model + non_xml_changes.append(change) + continue elif isinstance(change, ComputeModelChange): # get the values of model variables referenced by compute model changes if variable_values is None: model_etrees = {model.id: model_etree} - iter_variable_values = get_values_of_variable_model_xml_targets_of_model_change(change, sed_doc, - model_etrees, - working_dir) + iter_variable_values = \ + get_values_of_variable_model_xml_targets_of_model_change(change, sed_doc, model_etrees, working_dir) else: iter_variable_values = variable_values @@ -612,10 +621,15 @@ def apply_changes_to_xml_model(model, model_etree, sed_doc=None, working_dir=Non # Second pass: changes that need to be interpreter-based: for change in non_xml_changes: if isinstance(change, ModelAttributeChange): + if not set_value_executer: - raise NotImplementedError( - 'target ' + change.target + ' cannot be changed by XML manipulation, as the target ' - 'is not an attribute of a model element') + xpath_captures = regex.split(r"[\[|\]]", change.target) + if len(xpath_captures) != 3 or "@" not in xpath_captures[1] or xpath_captures[2] != "": + raise NotImplementedError( + 'target ' + change.target + ' cannot be changed by XML manipulation, as the target ' + 'is not an attribute of a model element') + else: + raise ValueError(f"SBML attribute to apply `{change.new_value}` to can not be figured out.") else: set_value_executer(change.model, change.target, None, change.new_value, preprocessed_task) diff --git a/tests/sedml/test_sedml_utils.py b/tests/sedml/test_sedml_utils.py index 10390b36..edbe7993 100644 --- a/tests/sedml/test_sedml_utils.py +++ b/tests/sedml/test_sedml_utils.py @@ -681,11 +681,19 @@ def test_errors(self): utils.apply_changes_to_xml_model(data_model.Model(changes=[change]), et, None, None) change = data_model.ModelAttributeChange( - target="/sbml:sbml/sbml:model/sbml:listOfSpecies/sbml:species[@id='Trim']", + target="/sbml:sbml/sbml:model/sbml:listOfSpecies/sbml:spesies[@id='Trim']", target_namespaces={'sbml': 'http://www.sbml.org/sbml/level2/version4'}, new_value='1.9') et = etree.parse(self.FIXTURE_FILENAME) - with self.assertRaises(NotImplementedError): + with self.assertRaises(ValueError): + utils.apply_changes_to_xml_model(data_model.Model(changes=[change]), et, None, None) + + change = data_model.ModelAttributeChange( + target="/sbml/model/listOfSpecies/spesies[@id='Trim']", + target_namespaces={'sbml': 'http://www.sbml.org/sbml/level2/version4'}, + new_value='1.9') + et = etree.parse(self.FIXTURE_FILENAME) + with self.assertRaises(ValueError): utils.apply_changes_to_xml_model(data_model.Model(changes=[change]), et, None, None) change = data_model.ModelAttributeChange( @@ -740,6 +748,13 @@ def test_errors(self): with self.assertRaisesRegex(ValueError, 'must match a single object'): utils.apply_changes_to_xml_model(data_model.Model(changes=[change]), et, None, None) + change = data_model.ModelAttributeChange( + target="/sbml:sbml/sbml:model/sbml:listOfSpecies/sbml:species[@id='Trim']", + target_namespaces={'sbml': 'http://www.sbml.org/sbml/level2/version4'}, + new_value='1.9') + et = etree.parse(self.FIXTURE_FILENAME) + utils.apply_changes_to_xml_model(data_model.Model(changes=[change]), et, None, None) + def test_apply_compute_model_change_new_value(self): change = data_model.ComputeModelChange( target="/model/parameter[@id='p1']/@value",