"""
Common interface to XML files which follow the entry id format,
this is an abstract class and is expected to
be used by other XML interface modules and not directly.
"""
from CIME.XML.standard_module_setup import *
from CIME.utils import expect, convert_to_string, convert_to_type
from CIME.XML.generic_xml import GenericXML
logger = logging.getLogger(__name__)
[docs]
class EntryID(GenericXML):
def __init__(self, infile=None, schema=None, read_only=True):
GenericXML.__init__(self, infile, schema, read_only=read_only)
self.groups = {}
[docs]
def get_default_value(self, node, attributes=None):
"""
Set the value of an entry to the default value for that entry
"""
value = self._get_value_match(node, attributes)
if value is None:
# Fall back to default value
value = self.get_element_text("default_value", root=node)
else:
logger.debug("node is {} value is {}".format(self.get(node, "id"), value))
if value is None:
logger.debug("For vid {} value is none".format(self.get(node, "id")))
value = ""
return value
[docs]
def set_default_value(self, vid, val):
node = self.get_optional_child("entry", {"id": vid})
if node is not None:
val = self.set_element_text("default_value", val, root=node)
if val is None:
logger.warning(
"Called set_default_value on a node without default_value field"
)
return val
[docs]
def get_value_match(
self,
vid,
attributes=None,
exact_match=False,
entry_node=None,
replacement_for_none=None,
):
"""Handle this case:
<entry id ...>
<values>
<value A="a1">X</value>
<value A="a2">Y</value>
<value A="a3" B="b1">Z</value>
</values>
</entry>
If replacement_for_none is provided, then: if the found text value would give a
None value, instead replace it with the value given by the replacement_for_none
argument. (However, still return None if no match is found.) This may or may not
be needed, but is in place to maintain some old logic.
"""
if entry_node is not None:
value = self._get_value_match(
entry_node,
attributes,
exact_match,
replacement_for_none=replacement_for_none,
)
else:
node = self.get_optional_child("entry", {"id": vid})
value = None
if node is not None:
value = self._get_value_match(
node,
attributes,
exact_match,
replacement_for_none=replacement_for_none,
)
logger.debug("(get_value_match) vid {} value {}".format(vid, value))
return value
def _get_value_match(
self, node, attributes=None, exact_match=False, replacement_for_none=None
):
"""
Note that the component class has a specific version of this function
If replacement_for_none is provided, then: if the found text value would give a
None value, instead replace it with the value given by the replacement_for_none
argument. (However, still return None if no match is found.) This may or may not
be needed, but is in place to maintain some old logic.
"""
# if there is a <values> element - check to see if there is a match attribute
# if there is NOT a match attribute, then set the default to "first"
# this is different than the component class _get_value_match where the default is "last"
values_node = self.get_optional_child("values", root=node)
if values_node is not None:
match_type = self.get(values_node, "match", default="first")
node = values_node
else:
match_type = "first"
# Store nodes that match the attributes and their scores.
matches = []
nodes = self.get_children("value", root=node)
for vnode in nodes:
# For each node in the list start a score.
score = 0
if attributes:
for attribute in self.attrib(vnode).keys():
# For each attribute, add to the score.
score += 1
# If some attribute is specified that we don't know about,
# or the values don't match, it's not a match we want.
if exact_match:
if attribute not in attributes or attributes[
attribute
] != self.get(vnode, attribute):
score = -1
break
else:
if attribute not in attributes or not re.search(
self.get(vnode, attribute), attributes[attribute]
):
score = -1
break
# Add valid matches to the list.
if score >= 0:
matches.append((score, vnode))
if not matches:
return None
# Get maximum score using either a "last" or "first" match in case of a tie
max_score = -1
mnode = None
for score, node in matches:
if match_type == "last":
# take the *last* best match
if score >= max_score:
max_score = score
mnode = node
elif match_type == "first":
# take the *first* best match
if score > max_score:
max_score = score
mnode = node
else:
expect(
False,
"match attribute can only have a value of 'last' or 'first', value is %s"
% match_type,
)
text = self.text(mnode)
if text is None:
# NOTE(wjs, 2021-06-03) I'm not sure when (if ever) this can happen, but I'm
# putting this logic here to maintain some old logic, to be safe.
text = replacement_for_none
return text
[docs]
def get_node_element_info(self, vid, element_name):
node = self.get_optional_child("entry", {"id": vid})
if node is None:
return None
else:
return self._get_node_element_info(node, element_name)
def _get_node_element_info(self, node, element_name):
return self.get_element_text(element_name, root=node)
def _get_type_info(self, node):
if node is None:
return None
val = self._get_node_element_info(node, "type")
if val is None:
return "char"
return val
[docs]
def get_type_info(self, vid):
vid, _, _ = self.check_if_comp_var(vid)
node = self.scan_optional_child("entry", {"id": vid})
return self._get_type_info(node)
# pylint: disable=unused-argument
[docs]
def check_if_comp_var(self, vid, attribute=None, node=None):
# handled in classes
return vid, None, False
def _get_default(self, node):
return self._get_node_element_info(node, "default_value")
# Get description , expect child with tag "description" for parent node
[docs]
def get_description(self, node):
return self._get_node_element_info(node, "desc")
# Get group , expect node with tag "group"
# entry id nodes are children of group nodes
[docs]
def get_groups(self, node):
groups = self.get_children("group")
result = []
nodes = []
vid = self.get(node, "id")
for group in groups:
nodes = self.get_children("entry", attributes={"id": vid}, root=group)
if nodes:
result.append(self.get(group, "id"))
return result
[docs]
def get_valid_values(self, vid):
node = self.scan_optional_child("entry", {"id": vid})
if node is None:
return None
return self._get_valid_values(node)
def _get_valid_values(self, node):
valid_values = self.get_element_text("valid_values", root=node)
valid_values_list = []
if valid_values:
valid_values_list = [item.lstrip() for item in valid_values.split(",")]
return valid_values_list
[docs]
def set_valid_values(self, vid, new_valid_values):
node = self.scan_optional_child("entry", {"id": vid})
if node is None:
return None
return self._set_valid_values(node, new_valid_values)
[docs]
def get_nodes_by_id(self, vid):
return self.scan_children("entry", {"id": vid})
def _set_valid_values(self, node, new_valid_values):
old_vv = self._get_valid_values(node)
if old_vv is None:
self.make_child("valid_values", text=new_valid_values)
logger.debug(
"Adding valid_values {} for {}".format(
new_valid_values, self.get(node, "id")
)
)
else:
vv_text = self.set_element_text("valid_values", new_valid_values, root=node)
logger.debug(
"Replacing valid_values {} with {} for {}".format(
old_vv, vv_text, self.get(node, "id")
)
)
current_value = self.get(node, "value")
valid_values_list = self._get_valid_values(node)
if current_value is not None and current_value not in valid_values_list:
logger.warning(
'WARNING: Current setting for {} not in new valid values. Updating setting to "{}"'.format(
self.get(node, "id"), valid_values_list[0]
)
)
self._set_value(node, valid_values_list[0])
return new_valid_values
def _set_value(self, node, value, vid=None, subgroup=None, ignore_type=False):
"""
Set the value of an entry-id field to value
Returns the value or None if not found
subgroup is ignored in the general routine and applied in specific methods
"""
expect(subgroup is None, "Subgroup not supported")
str_value = self.get_valid_value_string(node, value, vid, ignore_type)
self.set(node, "value", str_value)
return value
[docs]
def get_valid_value_string(self, node, value, vid=None, ignore_type=False):
valid_values = self._get_valid_values(node)
if ignore_type:
expect(
isinstance(value, str),
"Value must be type string if ignore_type is true",
)
str_value = value
return str_value
type_str = self._get_type_info(node)
str_value = convert_to_string(value, type_str, vid)
if valid_values and not str_value.startswith("$"):
expect(
str_value in valid_values,
"Did not find {} in valid values for {}: {}".format(
value, vid, valid_values
),
)
return str_value
[docs]
def set_value(self, vid, value, subgroup=None, ignore_type=False):
"""
Set the value of an entry-id field to value
Returns the value or None if not found
subgroup is ignored in the general routine and applied in specific methods
"""
val = None
root = (
self.root
if subgroup is None
else self.get_optional_child("group", {"id": subgroup})
)
node = self.get_optional_child("entry", {"id": vid}, root=root)
if node is not None:
val = self._set_value(node, value, vid, subgroup, ignore_type)
return val
[docs]
def get_values(self, vid, attribute=None, resolved=True, subgroup=None):
"""
Same functionality as get_value but it returns a list, if the
value in xml contains commas the list have multiple elements split on
commas
"""
results = []
node = self.scan_optional_child("entry", {"id": vid})
if node is None:
return results
str_result = self._get_value(
node, attribute=attribute, resolved=resolved, subgroup=subgroup
)
str_results = str_result.split(",")
for result in str_results:
# Return value as right type if we were able to fully resolve
# otherwise, we have to leave as string.
if "$" in result:
results.append(result)
else:
type_str = self._get_type_info(node)
results.append(convert_to_type(result, type_str, vid))
return results
# pylint: disable=arguments-differ
[docs]
def get_value(self, vid, attribute=None, resolved=True, subgroup=None):
"""
Get a value for entry with id attribute vid.
or from the values field if the attribute argument is provided
and matches
"""
root = (
self.root
if subgroup is None
else self.get_optional_child("group", {"id": subgroup})
)
node = self.scan_optional_child("entry", {"id": vid}, root=root)
if node is None:
return
val = self._get_value(
node, attribute=attribute, resolved=resolved, subgroup=subgroup
)
# Return value as right type if we were able to fully resolve
# otherwise, we have to leave as string.
if val is None:
return val
elif "$" in val:
return val
else:
type_str = self._get_type_info(node)
return convert_to_type(val, type_str, vid)
def _get_value(self, node, attribute=None, resolved=True, subgroup=None):
"""
internal get_value, does not convert to type
"""
logger.debug("(_get_value) ({}, {}, {})".format(attribute, resolved, subgroup))
val = None
if node is None:
logger.debug("No node")
return val
logger.debug(
"Found node {} with attributes {}".format(
self.name(node), self.attrib(node)
)
)
if attribute:
vals = self.get_optional_child("values", root=node)
node = vals if vals is not None else node
val = self.get_element_text("value", attributes=attribute, root=node)
elif self.get(node, "value") is not None:
val = self.get(node, "value")
else:
val = self.get_default_value(node)
if resolved:
val = self.get_resolved_value(val)
return val
[docs]
def get_child_content(self, vid, childname):
val = None
node = self.get_optional_child("entry", {"id": vid})
if node is not None:
val = self.get_element_text(childname, root=node)
return val
[docs]
def get_elements_from_child_content(self, childname, childcontent):
nodes = self.get_children("entry")
elements = []
for node in nodes:
content = self.get_element_text(childname, root=node)
expect(
content is not None,
"No childname {} for id {}".format(childname, self.get(node, "id")),
)
if content == childcontent:
elements.append(node)
return elements
[docs]
def add_elements_by_group(self, srcobj, attributes=None, infile=None):
"""
Add elements from srcobj to self under the appropriate
group element, entries to be added must have a child element
<file> with value "infile"
"""
if infile is None:
infile = os.path.basename(self.filename)
# First get the list of entries in srcobj with matching file children
nodelist = srcobj.get_elements_from_child_content("file", infile)
# For matchs found: Remove {<group>, <file>, <values>}
# children from each entry and set the default value for the
# new entries in self - putting the entries as children of
# group elements in file $file
for src_node in nodelist:
node = self.copy(src_node)
gname = srcobj.get_element_text("group", root=src_node)
if gname is None:
gname = "group_not_set"
# If group with id=$gname does not exist in self.groups
# then create the group node and add it to infile file
if gname not in self.groups.keys():
# initialize an empty list
newgroup = self.make_child(name="group", attributes={"id": gname})
self.groups[gname] = newgroup
# Remove {<group>, <file>, <values>} from the entry element
self.cleanupnode(node)
# Add the entry element to the group
self.add_child(node, root=self.groups[gname])
# Set the default value, it may be determined by a regular
# expression match to a dictionary value in attributes matching a
# value attribute in node
value = srcobj.get_default_value(src_node, attributes)
if value is not None and len(value):
self._set_value(node, value)
logger.debug("Adding to group " + gname)
return nodelist
[docs]
def cleanupnode(self, node):
"""
in env_base.py, not expected to get here
"""
expect(False, " Not expected to be here {}".format(self.get(node, "id")))
[docs]
def compare_xml(self, other, root=None, otherroot=None):
xmldiffs = {}
if root is not None:
expect(otherroot is not None, " inconsistant request")
f1nodes = self.scan_children("entry", root=root)
for node in f1nodes:
vid = self.get(node, "id")
logger.debug("Compare vid {}".format(vid))
f2match = other.scan_optional_child(
"entry", attributes={"id": vid}, root=otherroot
)
expect(f2match is not None, "Could not find {} in Locked file".format(vid))
if node != f2match:
f1val = self.get_value(vid, resolved=False)
if f1val is not None:
f2val = other.get_value(vid, resolved=False)
if f1val != f2val:
xmldiffs[vid] = [f1val, f2val]
elif hasattr(self, "_components"):
# pylint: disable=no-member
for comp in self._components:
f1val = self.get_value(
"{}_{}".format(vid, comp), resolved=False
)
if f1val is not None:
f2val = other.get_value(
"{}_{}".format(vid, comp), resolved=False
)
if f1val != f2val:
xmldiffs[f"{vid}_{comp}"] = [f1val, f2val]
else:
if node != f2match:
f1value_nodes = self.get_children("value", root=node)
for valnode in f1value_nodes:
f2valnodes = other.get_children(
"value",
root=f2match,
attributes=self.attrib(valnode),
)
for f2valnode in f2valnodes:
if (
self.attrib(valnode) is None
and self.attrib(f2valnode) is None
or self.attrib(f2valnode)
== self.attrib(valnode)
):
if other.get_resolved_value(
self.text(f2valnode)
) != self.get_resolved_value(
self.text(valnode)
):
xmldiffs[
"{}:{}".format(
vid, self.attrib(valnode)
)
] = [
self.text(valnode),
self.text(f2valnode),
]
return xmldiffs
[docs]
def overwrite_existing_entries(self):
# if there exist two nodes with the same id delete the first one.
for node in self.get_children("entry"):
vid = self.get(node, "id")
samenodes = self.get_nodes_by_id(vid)
if len(samenodes) > 1:
expect(
len(samenodes) == 2,
"Too many matchs for id {} in file {}".format(vid, self.filename),
)
logger.debug("Overwriting node {}".format(vid))
read_only = self.read_only
if read_only:
self.read_only = False
self.remove_child(samenodes[0])
self.read_only = read_only
def __iter__(self):
for node in self.scan_children("entry"):
vid = self.get(node, "id")
yield vid, self.get_value(vid)